feat(coding-dev): require domain values to be typed, not primitives
A value with rules of its own — a URL, a path, a closed set, an identifier — gets a type of its own. Passing it as a bare string discards those rules at the first function boundary and leaves every consumer re-deriving them. Added as a subsection of Architecture Preferences, with a table mapping the common cases to the type to reach for in any language, the reasoning, and the boundary rule: parse once at the edge, keep the type inward, serialize back only where the wire actually demands a string. Notes the counter-case, so this is not read as a mandate to wrap every string. python.md gains the concrete choices: yarl.URL (already present wherever aiohttp is), pathlib, StrEnum, and NewType for non-interchangeable identifiers.
This commit is contained in:
1 parent
3171595940
commit
b08fe50e92
2 files changed
+47
No files matched your search
@@ -48,6 +48,23 @@ disallow_untyped_defs = true
|
|||||||
- Third-party library missing stubs: install `types-*` if it exists, otherwise a narrowly
|
- Third-party library missing stubs: install `types-*` if it exists, otherwise a narrowly
|
||||||
scoped `# type: ignore[import-untyped]` with a comment — never a blanket module-level ignore.
|
scoped `# type: ignore[import-untyped]` with a comment — never a blanket module-level ignore.
|
||||||
|
|
||||||
|
### Value types over primitives
|
||||||
|
|
||||||
|
The general rule is in [`coding-dev.md` §7](skills-public/coding-dev.md); Python's concrete
|
||||||
|
choices are:
|
||||||
|
|
||||||
|
- **`yarl.URL` for URLs**, not f-string assembly. It ships with `aiohttp`, so any async HTTP
|
||||||
|
project already has it. `URL(base) / "api" / "v4" / "users"` joins without double slashes,
|
||||||
|
and `.with_query({...})` escapes parameters that string formatting would corrupt.
|
||||||
|
- **`pathlib.Path` for paths**, never `os.path` + `str`. Ruff's `PTH` rules enforce this;
|
||||||
|
keep `os.fdopen` where a descriptor rather than a name is being adopted.
|
||||||
|
- `enum.StrEnum` (3.11+) where a closed set must also serialize as a string, so the wire
|
||||||
|
format stays a string without the code comparing bare literals.
|
||||||
|
- `typing.NewType` for identifiers that should not be interchangeable — a `UserId` and a
|
||||||
|
`RepoId` are both `int` and nothing otherwise stops them being swapped.
|
||||||
|
- Parse at the boundary: take `str` in the public signature if callers supply one, convert
|
||||||
|
immediately, and keep the real type internal.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Linting
|
## 3. Linting
|
||||||
|
|||||||
@@ -257,6 +257,36 @@ Run the **whole** gate, not the convenient part of it. A typical full gate:
|
|||||||
- **A search-and-replace that matches nothing is a silent failure.** Assert the target string
|
- **A search-and-replace that matches nothing is a silent failure.** Assert the target string
|
||||||
exists before replacing, and verify the result afterward.
|
exists before replacing, and verify the result afterward.
|
||||||
|
|
||||||
|
### Model domain values as types, not primitives
|
||||||
|
|
||||||
|
**A value with rules of its own gets a type of its own.** Passing it around as a bare string,
|
||||||
|
int, or dict throws those rules away at the first function boundary and makes every consumer
|
||||||
|
re-derive them.
|
||||||
|
|
||||||
|
| Value | Use | Not |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| URL | the platform's URL type (`yarl.URL`, `Uri`, `url::Url`, `URL`) | `str` + f-string assembly |
|
||||||
|
| Filesystem path | `Path` / `PathBuf` / `std::filesystem::path` | `str` + manual separators |
|
||||||
|
| Closed set of values | enum / union of literals | bare strings compared with `==` |
|
||||||
|
| Structured record | struct, dataclass, or typed record | `dict[str, Any]` / `map[string]any` |
|
||||||
|
| Quantity with a unit | a wrapper carrying the unit | a bare number and a naming convention |
|
||||||
|
| Identifier of a specific kind | a distinct newtype | a `str` that any other id also fits |
|
||||||
|
|
||||||
|
**Why:** the type is where the rules live. A URL type knows how to join a path without
|
||||||
|
producing a double slash, escapes query parameters, and cannot be concatenated into something
|
||||||
|
malformed. A path type knows the platform separator. An enum cannot hold a typo. Each one
|
||||||
|
turns a class of runtime bug into something the compiler or checker rejects — and makes the
|
||||||
|
signature say what it accepts instead of `str`.
|
||||||
|
|
||||||
|
- **Convert at the boundary.** Parse incoming strings into the real type once, at the edge,
|
||||||
|
and keep the type inward. Serialize back to a string only where the wire or the filesystem
|
||||||
|
actually demands one.
|
||||||
|
- **Don't reach for a wrapper where the primitive genuinely is the value** — a person's name
|
||||||
|
is a string. The test is whether the value has invariants, a format, or operations that
|
||||||
|
callers keep re-implementing.
|
||||||
|
- Retrofit opportunistically: convert what you touch rather than rewriting a stringly-typed
|
||||||
|
codebase in one pass.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Scope Discipline
|
## 8. Scope Discipline
|
||||||
|
|||||||
Reference in new issue
Block a user