This repository was archived by the owner on Oct 12, 2026. It is now read-only.
Repository navigation
feat: validate a whole form in one request - #8
Merged
Merged
Conversation
POST /validate/batch takes a list of values, each with its own document type, and answers one result per item plus a summary. A registration form with eight fields was eight round trips; now it is one. The precondition was removing the duplication that would have made this two implementations of the same thing. Every validator now lives in one table in utils/dispatch.go, and both endpoints read from it — so a document added there appears on both at once and neither can quietly fall behind the other. The single-value handler lost its fourteen-branch if/else chain in the process, and its parameter counting is derived from the same table rather than a list kept by hand next to it. Design decisions worth arguing with: Items are a list, not an object keyed by document type. A form can carry two CPFs — a customer and a guarantor — and an object would silently keep only the last. Each item may carry an id, echoed back untouched, so results line up with the caller's own fields without depending on ordering. A batch that ran answers 200 even when items failed validation. The single endpoint answers 422 for an invalid value because there the value is the whole request; here it is one line of many, and failing the response would discard the results that did pass. An unknown document type is reported as UNKNOWN_KEY on that item rather than as an invalid value, and does not fail the batch. Calling it "invalid" would suggest the value had been checked and rejected, and discarding nine good results over one typo helps nobody. Bounded on both axes: at most 100 items, and the body is capped before parsing so a large request cannot exhaust memory ahead of the item check. Unknown JSON fields are refused rather than ignored — a caller who misspells "items" deserves to be told, not handed an empty summary. The parser error is never echoed. It can quote the body, and the body is exactly the personal data that must not travel back out in an error; a test asserts a submitted CPF cannot be found in the failure message.
jeferson0306
added a commit
that referenced
this pull request
Sep 4, 2026
The three left out of the catalogue because I could not settle which published algorithm was authoritative. brazilian-utils — already the reference for PIS and the voter ID — covers all three, so the same method applies: its vectors, then a differential run against a flat transcription of its algorithm. CNH's two check digits are coupled. When the first overflows past nine it is written as zero and the second is computed two lower to compensate. Treating them independently, which is what several published implementations do, accepts numbers the Detran would reject. RENAVAM runs its weights from the right, cycling two through nine, rather than the left-to-right descending series the rest of these documents use. Nine digits is the old format and eleven the current one; the shorter is the longer with leading zeros, so both are accepted and normalised to eleven. The boleto is two checks, not one. The linha digitável is the barcode's 44 digits shuffled into three typed groups, each carrying its own module-10 digit so that someone copying from paper is told which group they mistyped. Only after those pass is the barcode reassembled — the pieces are in a different order in each — and its own module-11 digit checked, which sits inside the barcode rather than at the end. A 48-digit arrecadação slip is a different structure and is not implemented. It is reported as not checked rather than as invalid: calling it invalid would be a lie about a number that may well be good. One deliberate deviation from the reference in each of the first two: stray characters are rejected rather than stripped, and a run of one repeated digit is not a registration. Both match what the rest of this service already does. Wiring was three lines, because #8 moved the validators into one table that both endpoints read. A test asserts all three are reachable through the batch without any further wiring — that table earning its keep.
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
A registration form with eight fields was eight round trips. Now it is one.
The precondition
Adding a second endpoint over the same validators would have created two implementations of the same dispatch, free to drift apart. So every validator now lives in one table in
utils/dispatch.go, and both endpoints read from it:A document added there appears on both endpoints at once. The single-value handler lost its fourteen-branch
if/elsechain in the process, and its parameter counting is now derived from the same table rather than from a list kept by hand next to it — which is what let CNPJ slip past the one-per-request guard when it was added.The existing handler tests passed unchanged through the refactor, which is the evidence that behaviour was preserved.
Design decisions worth arguing with
Items are a list, not an object keyed by document type. A form can carry two CPFs — a customer and a guarantor — and an object would silently keep only the last. Each item may carry an
id, echoed back untouched, so results line up with the caller's own fields without depending on ordering.A batch that ran answers 200 even when items failed validation. The single endpoint answers 422 for an invalid value because there the value is the whole request. Here it is one line of many, and failing the response would discard the results that did pass. The summary tells you what happened:
An unknown document type is
UNKNOWN_KEYon that item, not an invalid value, and does not fail the batch. Calling it "invalid" would suggest the value had been checked and rejected; discarding nine good results over one typo helps nobody.Bounded on both axes
At most 100 items — a form has tens of fields, not thousands, and anything past that is a mistake or an attempt to use the service as free compute. The body is capped at 1 MB before parsing, so a large request cannot exhaust memory ahead of the item check. Unknown JSON fields are refused rather than ignored: a caller who misspells
itemsdeserves to be told, not handed an empty summary.The parser error is never echoed
A JSON decoding error can quote the body, and the body is exactly the personal data that must not travel back out. A test asserts a submitted CPF cannot be found anywhere in the failure message — the same rule as the logging fix in #4, applied to the other direction of travel.
Tests
Eight new cases;
handlerscoverage 79% → 94%. CORS now advertises POST, and the Swagger spec carries the new route.