Offline validation of company identifiers — VAT numbers, national registration numbers, LEI, EUID and thirty four other kinds — for Swift.
import EntID
// `vat-be-normalization-004`. Synthetic, produced by the generator of
// DATA_POLICY.md section 4 — it designates no company, and none is needed
// here: a README example demonstrates an API, not what a register issues.
let report = EntIDEngine.default.validate(
IdentifierInput(kind: "vat", value: "BE 0123.456.749")
)
report.canonicalValue // "BE0123456749"
report.countryCode // "BE"
report.format.status // .valid
report.checksum.status // .validRules version 2026.08.38: 94 definitions across 37 countries, 37 identifier
kinds, and the full shared conformance corpus of 676 cases passing.
This library answers two questions and refuses to imply a third.
format valid— the shape is compatible with a documented variant.checksum valid— the documented internal check is satisfied.- Existence is not answered. No registry is consulted, here or anywhere in this version. A number that validates may belong to no one.
There is deliberately no isValid on a report. A format that validates with a
checksum its authority has never published is neither fully validated nor
invalid, and a single boolean would have to lie about one of the two:
// `cegjegyzekszam-hu-valid-001`, synthetic like the one above.
let report = engine.validate(IdentifierInput(kind: "cegjegyzekszam", value: "0123456789"))
report.isFormatValid // true
report.isChecksumValid // false
report.isFullyValidated // false
report.isInvalid // false ← not knowing is not a rejection
report.checksum.reasonCode // .checksumNotPublishedWhere no documented rule applies, the answer is unsupported, never invalid.
Refusing a valid identifier is the most serious defect this project recognises,
and turning an absence of knowledge into a rejection is how it happens. An
unknown kind, a country with no rule, an unpublished checksum algorithm — each
is reported as what it is.
Every identifier printed in this README is synthetic and names the
conformance case it is quoted from. engine.md section 12.2.1 and
DATA_POLICY.md section 3 separate the two demonstrations: a synthetic value
proves an algorithm, a real one proves that a rule describes what a register
issues. This file demonstrates an API, so synthetic is the correct choice — and
a value written from memory is neither, and is forbidden everywhere.
.package(url: "https://github.com/entid-org/entid-swift.git", from: "0.1.0").product(name: "EntID", package: "entid-swift")Requires Swift 6.1. Declared for macOS 13+, iOS 16+, tvOS 16+ and watchOS 9+;
CI builds and tests on macOS with the current toolchain and with Swift 6.1, and
runs the library suite on an iOS simulator. The conformance corpus is not run
there: the runner drives the testee as a subprocess, which a simulator has no
Process for, so that target is compiled out anywhere but macOS rather than
skipped at run time.
The EntID library links nothing. No Protobuf, no HTTP, no UIKit, no
AppKit; the binary of a consumer carries none of them. SwiftPM does still
resolve this package's whole manifest, so swift-protobuf is fetched into
.build/checkouts — it belongs to entid-gen and the conformance
tooling, which are separate targets a consumer never builds and never links.
Tools/audit-dependencies.py fails CI if a second name ever appears there.
All four are synchronous, and will stay synchronous.
let engine = EntIDEngine.default
engine.canonicalize(input) // normalize only, no rule runs
engine.validate(input) // format, then checksum
engine.validateFormat(input) // format only; checksum reports not_run/not_requested
engine.validateChecksum(input) // exactly what validate returns, named for the call site
engine.rulesInfo() // versions, coverage, kinds
engine.capabilities() // the frozen capability ids the rules requirevalidateChecksum exists for readability, not to skip the format: a checksum is
never run on a value whose format was not validated.
engine.validate(input) // the definition's own default
engine.validate(input, options: .init(profile: .compatible)) // accept documented historical variants
engine.validate(input, options: .init(profile: .strictCurrent)) // only currently issued variantsLeaving the profile out is not the same as asking for compatible: it is what
lets a definition apply its own default. An API that filled it in would make
that default unreachable.
This engine does not interpret rules at runtime. It is not a virtual machine.
A generator runs when the library is built: it reads the attested rule
bundle, applies the twenty five load checks of the specification, and emits
Swift. What ships is that emitted code, a small set of primitives it calls, and
this API. There is no Protobuf decoder, no IR interpreter and no .binpb in the
package.
spec/entid-rules.binpb ──▶ entid-gen ──▶ Sources/EntID/Generated/*.swift
(attested input) (25 checks) (committed, reviewable)
The generated code is committed, so building this package — as a consumer or in CI — needs no network and no access to the specification repository. Regeneration is a maintainer's deliberate act:
make verify-lock # every digest rules.lock attests
make generate # recompile the bundle to Swift
make generated-check # fail if the committed output is staleConsequently there is no initializer taking a bundle at runtime. A custom ruleset goes through the generator, at build time.
EntIDEngine is a Sendable value with no stored property. Sharing it
across tasks needs no lock and no @unchecked escape hatch, because there is
nothing to protect. Every public type is Sendable; nothing is cached between
calls; no global is mutable.
await withTaskGroup(of: ValidationReport.self) { group in
for input in inputs { group.addTask { EntIDEngine.default.validate(input) } }
}- Positions and lengths are counted in Unicode code points, never in
grapheme clusters and never in UTF-16 units.
String.countcounts grapheme clusters and is used nowhere in this package. - Whitespace is the frozen
whitespace_v1table, written out. It is never delegated to the platform's Unicode tables, whose versions differ. - Uppercasing maps only
a...zand consults no locale. - Raw input is bounded at 1024 UTF-8 bytes. A longer input is refused
without being processed, reported as
unsupported/inputTooLong— a safety bound, never a business verdict.
ReasonCode.invalidEncoding stays in the registry and is unreachable through
this API. Every engine pins that step with a native test naming the malformed
form its own string type admits — an invalid byte where strings are bytes, an
unpaired surrogate where they are UTF-16 code units. Swift's String admits
neither: String(bytes:encoding:) refuses an invalid byte, String(decoding:)
repairs it to U+FFFD, and Unicode.Scalar(0xD800) is nil. Adding a
byte-oriented entry point that existed only to reach the branch would widen the
public API to serve a reason code rather than a caller.
There is no registry type in this package, not even an experimental one. A public type is a commitment SemVer freezes.
What is decided now is the shape of the hole: local validation stays synchronous
permanently, validate will never call a registry, and a lookup — which carries
an API token and must never be possible from a browser — will arrive in a
separate, server-only module when it is specified.
make docs # DocC archive for the EntID target
make help # every task CI runsSPEC-ISSUES.md— contradictions found in the specification while implementing, and the reading this engine follows for each.SECURITY.md— threat model, reporting, rule revocation.CONTRIBUTING.md— what a change has to answer.
Conformance is not a suite rewritten in Swift, and it is not this package's
verdict to give. The runner comes from the specification repository, pinned
to the commit rules.lock records under source_commit — the same commit as
the corpus, so a corpus can never be judged by another release's comparator:
make conformance # rules 2026.08.38: 676 cases, 676 matched, 0 differedwhich is
go run github.com/entid-org/spec/cmd/conformance-runner@<source_commit> \
-corpus spec/entid-conformance.binpb -- .build/debug/entid-testeeA Go toolchain in CI is the only prerequisite. It is a build tool: nothing about it enters the published package or its dependencies.
What this package writes is the testee — an executable that reads requests, calls the public API and writes responses — and the tests proving it does not cheat. It does not read the corpus, does not interpret an expected result, and does not behave differently depending on which case it was handed; each of those is a test, both by reading its source and by observing it answer identically from a directory holding no corpus, under a borrowed case identifier, and in a shuffled order.
There is deliberately no comparator here. An engine that judges its own results can declare itself conformant by comparing too weakly — forgetting a field, or treating an absent field as an empty one.
The thirty five load_ruleset cases address the generator rather than the
engine: a truncated bundle, one carrying a call cycle, or one whose subject node
is built from the subject it defines must make generation fail. The rest address
the engine over the wire protocol.
Every digest in rules.lock is checked before a byte of the bundle is trusted:
./Tools/verify-lock.shThe generator verifies the digest of the file it received and never re-serializes a decoded message to recompute one, because Protobuf is not a canonical serialization.
Apache-2.0. See LICENSE.