Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 90 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,101 @@
# Gatepost spec
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/gatepost-dev/.github/main/brand/gatepost-lockup-dark.svg">
<img src="https://raw.githubusercontent.com/gatepost-dev/.github/main/brand/gatepost-lockup.svg" alt="Gatepost" width="220">
</picture>
</p>

The shared contract for every Gatepost SDK: the postcode grammar, the data files, the test vectors, the coding standards and the repo templates.
<h1 align="center">Gatepost spec</h1>

<p align="center">The shared contract for every Gatepost SDK: the postcode grammar, the data files, the test vectors and the coding standards.</p>

<p align="center">
<a href="https://github.com/gatepost-dev/spec/actions/workflows/ci.yml"><img src="https://github.com/gatepost-dev/spec/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI status"></a>
<a href="https://github.com/gatepost-dev/spec/blob/main/LICENSE"><img src="https://img.shields.io/badge/licence-Apache--2.0-blue?style=flat" alt="Licence: Apache-2.0"></a>
</p>

<p align="center">
<a href="https://gatepost-dev.github.io/docs/">Docs</a>
&middot;
<a href="https://gatepost-dev.github.io/docs/playground/">Playground</a>
&middot;
<a href="https://gatepost-dev.github.io/docs/spec/grammar/">Grammar</a>
&middot;
<a href="https://github.com/gatepost-dev/.github/blob/main/CONTRIBUTING.md">Contributing</a>
&middot;
<a href="https://github.com/gatepost-dev/spec/discussions">Discussions</a>
</p>

> Unofficial. Not made or endorsed by NIPOST.

This repo defines how every Gatepost SDK reads and writes Nigeria's digital postcodes, and how every Gatepost client calls NIPOST's gateway. An SDK is correct when it passes the shared vectors and the contract scenarios. If `grammar.md` and a vector disagree, `grammar.md` wins, and the disagreement is a bug.

The current version is in [`VERSION`](VERSION).

## A postcode in three forms

| Form | Example | Use |
|---|---|---|
| Compact | `EK01A03FK01` | storage and comparison |
| Canonical | `EK-01-A03-FK-01` | APIs and logs |
| Display | `EK 01 A03 FK 01` | screens and print |

A postcode has five segments, from large to small: state, LGA, district, area and unit. [`grammar.md`](grammar.md) has the rules.

## One test vector

This case is `contains-001` in [`vectors/contains.json`](vectors/contains.json).

```json
{
"id": "contains-001",
"description": "finds a full code in its district",
"input": {
"prefix": "EK-01-A03",
"code": "EK-01-A03-FK-01"
},
"options": {},
"expect": {
"value": true
}
}
```

Every SDK runs the cases in [`vectors/`](vectors).

## Contents

| Path | Holds |
|---|---|
| `grammar.md` | how every SDK reads and writes postcodes, and the public interface |
| `client.md` | how every client calls NIPOST's gateway: results, errors, retries and the queue |
| `openapi/` | NIPOST's gateway, completed with the responses that its own file leaves out |
| `fixtures/` | synthetic gateway responses and keys for the mock server |
| `contract/` | shared client test cases that every client must pass against the mock server |
| `data/` | state codes, GPS accuracy limits and the postcode format |
| `vectors/` | shared test cases that every SDK must pass |
| `standards/` | the coding standards and one file for each language |
| `CONTEXT.md` | the glossary |
| `templates/` | files for new repos |
| `scripts/` | `check-tells`, the vector builder, a plain reading of the grammar that checks each vector, and the checks of the OpenAPI file, the fixtures and the scenarios |

## Check

Run `make check`. It needs Python 3.11 or later and `uv`.
| [`grammar.md`](grammar.md) | how every SDK reads and writes postcodes, and the public interface |
| [`client.md`](client.md) | how every client calls NIPOST's gateway: results, errors, retries and the queue |
| [`openapi/`](openapi) | NIPOST's gateway, completed with the responses that its own file leaves out |
| [`fixtures/`](fixtures) | synthetic gateway responses and keys for the mock server |
| [`contract/`](contract) | shared client test cases that every client must pass against the mock server |
| [`data/`](data) | state codes, GPS accuracy limits and the postcode format |
| [`vectors/`](vectors) | shared test cases that every SDK must pass |
| [`standards/`](standards) | the coding standards and one file for each language |
| [`CONTEXT.md`](CONTEXT.md) | the glossary |
| [`templates/`](templates) | files for new repos |
| [`scripts/`](scripts) | `check-tells`, the vector builder, a plain reading of the grammar that checks each vector, and the checks of the OpenAPI file, the fixtures and the scenarios |

## Who uses it

| Repo | Language | Implements |
|---|---|---|
| [js](https://github.com/gatepost-dev/js) | TypeScript | the grammar and the client |
| [php](https://github.com/gatepost-dev/php) | PHP | the grammar and the client |

The [docs site](https://gatepost-dev.github.io/docs/) shows the same grammar and client contract as pages for readers.

## Change the spec

A change to the public interface of an SDK starts here. Open an issue first. Then read [`CONTRIBUTING.md`](https://github.com/gatepost-dev/.github/blob/main/CONTRIBUTING.md). Run `make check` before you open a pull request. It needs Python 3.11 or later and `uv`.

## Support

Ask questions in [GitHub Discussions](https://github.com/gatepost-dev/spec/discussions). Report bugs in [GitHub Issues](https://github.com/gatepost-dev/spec/issues). Report security problems through the [private form](https://github.com/gatepost-dev/spec/security/advisories/new).

## Licence

Apache-2.0. See `LICENSE` and `NOTICE`.
Apache-2.0. See [`LICENSE`](LICENSE) and [`NOTICE`](NOTICE).
29 changes: 29 additions & 0 deletions scripts/tests/test_readme.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# SPDX-FileCopyrightText: 2026 The Gatepost authors
# SPDX-License-Identifier: Apache-2.0
"""Tests that the README example is a real vector, and that the README keeps its header."""

import json
import re
import unittest
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]
NOTICE = "> Unofficial. Not made or endorsed by NIPOST."


class ReadmeTest(unittest.TestCase):
def test_example_vector_is_the_case_in_the_vectors_file(self) -> None:
readme = (ROOT / "README.md").read_text(encoding="utf-8")
block = re.search(r"```json\n(.*?)\n```", readme, re.DOTALL)
self.assertIsNotNone(block)
shown = json.loads(block.group(1) if block else "")
vectors = json.loads((ROOT / "vectors" / "contains.json").read_text(encoding="utf-8"))
case = next(item for item in vectors["cases"] if item["id"] == shown["id"])
self.assertEqual(shown, case)

def test_readme_and_template_keep_the_notice_after_the_header_block(self) -> None:
for name in ("README.md", "templates/README.md"):
with self.subTest(name=name):
text = (ROOT / name).read_text(encoding="utf-8")
self.assertIn(NOTICE, text)
self.assertLess(text.index('<h1 align="center">'), text.index(NOTICE))
7 changes: 4 additions & 3 deletions standards/CODING_STANDARDS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Gatepost coding standards

Version 1.1, 3 Oct 2026.
Version 1.2, 3 Oct 2026.

These standards apply to every Gatepost repo, in every language. Each repo also follows its language file in `standards/languages/`. Together, the two files are the standard for that repo.

Expand Down Expand Up @@ -169,8 +169,9 @@ The command exits with 0 when all files pass, with 1 when a rule is broken, and
## Documentation

- **DOC-1 MUST.** Each public symbol has a doc comment. It gives a one-line summary, each parameter, each error and one example.
- **DOC-2 MUST.** Each README follows `templates/README.md`.
- **DOC-3 MUST.** Each code example in a README or on the docs site runs in CI.
- **DOC-2 MUST.** Each README follows `templates/README.md`, with the sections in its order. It starts with a centred header block: the logo in a `<picture>` element, the name, one tagline, the badges and a row of links. The line "Unofficial. Not made or endorsed by NIPOST." follows as a quote block. The header block is the only place for HTML. A package README has 100 to 130 lines, with an install command, two short examples, a table of the main functions, 4 to 8 feature bullets and the requirements. The docs site holds the rest. A monorepo root README holds a table of packages and little else. A badge shows a thing that exists today, so the version badge waits for the registry. Questions go to GitHub Discussions, bugs to GitHub Issues and security reports to the private form. Steps for maintainers do not belong in a README. Link the contributing guide.
- **DOC-2a MUST.** An install command is the real command. If the package is not on its registry, a plain sentence under the command says so. The first release removes that sentence.
- **DOC-3 MUST.** Each code example in a README or on the docs site runs in CI. The quickstart has 10 lines or fewer.
- **DOC-4 MUST.** Prose uses plain English. Write sentences of 25 words or fewer, with one idea in each. Use the active voice, and name who does the action. Use the plainest word, and use one word for one meaning. Keep every article and connector. Write a verb, not a noun that is made from it, and do not put more than three nouns in a row. Do not use phrasal verbs, semicolons, stacked hedges, em dashes or marketing words such as "powerful" and "seamless". Do not open with filler or close with a summary. Use British spelling.
- **DOC-5 MUST.** Each package has a changelog that change files generate. It follows the Keep a Changelog format, unless the release tool writes its own format. Changesets, the tool for npm packages, writes its own format.
- **DOC-6 SHOULD.** Record a decision as an ADR when it is hard to reverse, surprising, and the result of a real trade-off. An ADR is a short file in `docs/adr/` that gives the context, the decision and the reason.
Expand Down
74 changes: 56 additions & 18 deletions templates/README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,58 @@
# {{PACKAGE_NAME}}

{{ONE_SENTENCE_SUMMARY}}
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/gatepost-dev/.github/main/brand/gatepost-lockup-dark.svg">
<img src="https://raw.githubusercontent.com/gatepost-dev/.github/main/brand/gatepost-lockup.svg" alt="Gatepost" width="220">
</picture>
</p>

<h1 align="center">{{PACKAGE_NAME}}</h1>

<p align="center">{{ONE_SENTENCE_SUMMARY}}</p>

<p align="center">
<a href="{{CI_URL}}"><img src="{{CI_BADGE_URL}}" alt="CI status"></a>
<a href="{{REGISTRY_URL}}"><img src="{{VERSION_BADGE_URL}}" alt="Version"></a>
<a href="https://github.com/gatepost-dev/{{REPO_NAME}}/blob/main/LICENSE"><img src="https://img.shields.io/badge/licence-Apache--2.0-blue?style=flat" alt="Licence: Apache-2.0"></a>
<a href="{{SCORECARD_URL}}"><img src="{{SCORECARD_BADGE_URL}}" alt="OpenSSF Scorecard"></a>
</p>

<p align="center">
<a href="https://gatepost-dev.github.io/docs/">Docs</a>
&middot;
<a href="https://gatepost-dev.github.io/docs/playground/">Playground</a>
&middot;
<a href="{{DOCS_PAGE_URL}}">{{DOCS_PAGE_NAME}}</a>
&middot;
<a href="https://github.com/gatepost-dev/.github/blob/main/CONTRIBUTING.md">Contributing</a>
&middot;
<a href="https://github.com/gatepost-dev/{{REPO_NAME}}/discussions">Discussions</a>
</p>

> Unofficial. Not made or endorsed by NIPOST.

[![CI]({{CI_BADGE_URL}})]({{CI_URL}})
[![Version]({{VERSION_BADGE_URL}})]({{REGISTRY_URL}})
[![Licence](https://img.shields.io/badge/licence-Apache--2.0-blue)](LICENSE)
```{{LANGUAGE_ID}}
{{QUICKSTART_CODE}}
```

{{ONE_SENTENCE_ABOUT_THE_OUTPUT}}

## Install

```{{LANGUAGE_ID}}
```sh
{{INSTALL_COMMAND}}
```

## Quickstart
{{UNPUBLISHED_NOTE}}

## Use

```{{LANGUAGE_ID}}
{{QUICKSTART_CODE}}
{{SECOND_EXAMPLE_CODE}}
```

{{ONE_SENTENCE_ABOUT_THE_OUTPUT}}
| Function | What it does |
|---|---|
| `{{FUNCTION}}` | {{WHAT_IT_DOES}} |

## What it does

Expand All @@ -39,21 +71,27 @@ The guide and the API reference are at {{DOCS_URL}}.

## Support

Ask questions in GitHub Discussions. Report bugs in GitHub Issues. Report security problems privately, as `SECURITY.md` describes.
Ask questions in [GitHub Discussions](https://github.com/gatepost-dev/{{REPO_NAME}}/discussions). Report bugs in [GitHub Issues](https://github.com/gatepost-dev/{{REPO_NAME}}/issues). Report security problems through the [private form](https://github.com/gatepost-dev/{{REPO_NAME}}/security/advisories/new).

## Contributing
## Develop

Read `CONTRIBUTING.md` before you open a pull request.
Read [`CONTRIBUTING.md`](https://github.com/gatepost-dev/.github/blob/main/CONTRIBUTING.md) before you open a pull request. {{REPO_STEPS_LINK}}

## Licence

Apache-2.0. See `LICENSE` and `NOTICE`.
Apache-2.0. See [`LICENSE`](LICENSE) and [`NOTICE`](NOTICE).

<!--
Template rules, from standards/CODING_STANDARDS.md:
- DOC-2: keep these sections in this order.
- DOC-3: CI runs the Quickstart code. Keep it at 10 lines or fewer.
- LIC-3: keep the "Unofficial" line.
- TELL-14: no emojis. Use at most three badges.
- DOC-2: keep the header block, the notice and these sections in this order. Use HTML only in the header block.
- DOC-2: keep a package README to 100 to 130 lines. A monorepo root README holds a package table and little else.
- DOC-3: CI runs the quickstart and the second example. Keep the quickstart at 10 lines or fewer.
- LIC-3: keep the "Unofficial" line, directly after the header block.
- TELL-14: no emojis. Show a badge only for a thing that exists today.
- Badges: CI, version, licence, Scorecard, in that order. Delete the version badge until the package is on its registry.
Delete the Scorecard badge unless the repo has a scorecard workflow that has run and published.
- Install: keep the real command. While the package is not on its registry, {{UNPUBLISHED_NOTE}} is one plain sentence:
"The first alpha is not published yet." Delete it in the commit that publishes the package.
- Delete {{REPO_STEPS_LINK}} if the repo has no steps of its own.
Delete this comment when you fill in the template.
-->