Only the repositories touch the database
When every query goes through a repository, a schema change is made in one place and the queries can be read in one folder. import-linter writes "only these modules may import that one" as a protected contract. In Rulebearing it is a forbidden rule whose from.pathNot names the modules that may import the protected one.
- Plan: plan 0004, Step 5 (the import-linter contract kinds)
- Source: design ยง import-linter contracts
- Decision: ADR-0013 (the import-linter mapping)
- Requirement: FR-REACH-01
The rule
Only shop/repositories/, and shop/db/ itself, may import anything under shop/db/. The check is on direct imports, as import-linter's protected contract checks them: the web layer reaching the database through a repository is the point of the rule, not a breach of it.
rulebearing.yaml$schema: https://benbahrenburg.github.io/rulebearing/schema/config-v1.json
languages:
python: {}
rules:
dependencies:
forbidden:
- name: only-repositories-import-the-database
comment: "Every query goes through a repository, so a schema change is made in one place. plan:0004-wave-4-reach"
fix: "Call a function in shop/repositories/ instead; if none fits, add one there that runs the query."
severity: error
from: { pathNot: "^shop/(db|repositories)/" }
to: { path: "^shop/db/" }
examples:
forbidden:
- "shop/web/views.py -> shop/db/session.py"
allowed:
- "shop/repositories/orders.py -> shop/db/session.py"
- "shop/db/session.py -> shop/db/pool.py"
- "shop/web/views.py -> shop/repositories/orders.py"
The file is rulebearing.yaml. Its examples are the edges rulebearing test checks the rule against.
The same rule for dependency-cruiser
rulebearing config convert rulebearing.yaml --to dependency-cruiser writes the rule in dependency-cruiser's format and names what it dropped: the languages.python block, the $schema, the fix and the examples.
.dependency-cruiser.json{
"forbidden": [
{
"name": "only-repositories-import-the-database",
"comment": "Every query goes through a repository, so a schema change is made in one place. plan:0004-wave-4-reach",
"to": {
"path": "^shop/db/"
},
"severity": "error",
"from": {
"pathNot": "^shop/(db|repositories)/"
}
}
]
}
dropped (stderr of config convert)dropped 4 item(s) dependency-cruiser cannot hold:
languages.python: dependency-cruiser reads TypeScript and JavaScript only
$schema: points at the native schema
forbidden[only-repositories-import-the-database].fix: a Rulebearing rule field
forbidden[only-repositories-import-the-database].examples: a Rulebearing rule field
The files are converted/.dependency-cruiser.json and converted/dropped.txt.
The failing edge
In repo/, shop/web/views.py shows an order through the repository, but counts the orders with a query of its own:
| Severity | Rule | From | To | Type | Through |
|---|---|---|---|---|---|
| error | only-repositories-import-the-database | shop/web/views.py | shop/db/session.py | dependency |
only-repositories-import-the-databasefix: Call a function in shop/repositories/ instead; if none fits, add one there that runs the query.
The fix
Put the query in the repository and call it:
# shop/repositories/orders.py
def count_orders() -> int:
return len(session.query("select id, total from orders"))
# shop/web/views.py
from shop.repositories import orders
def count_orders() -> int:
return orders.count_orders()
From an import-linter contract
The same question as import-linter's protected contract, in repo/.importlinter:
.importlinter[importlinter]
root_package = shop
[importlinter:contract:db-protected]
name = Only the repositories open database sessions
type = protected
protected_modules =
shop.db
allowed_importers =
shop.repositories
rulebearing import import-linter translates it into allowed rules, as the design's table maps it: one naming the importers the contract allows, one for importers outside the root package, and, because dependency-cruiser's allowed rules are one allow-list for the whole file, one that allows every import of a module no protected contract names. An import no allowed rule covers is reported as not-in-allowed, at allowedSeverity: error:
rulebearing import import-linter# Imported from .importlinter by `rulebearing import import-linter`.
# Each rule's comment names its import-linter contract; the contract's options are written above its first rule.
$schema: https://benbahrenburg.github.io/rulebearing/schema/config-v1.json
languages:
python:
roots: ["."]
rules:
dependencies:
allowed:
# import-linter contract `db-protected` (protected): Only the repositories open database sessions
# allowed_importers = shop.repositories
# protected_modules = shop.db
- name: db-protected
comment: "import-linter contract: Only the repositories open database sessions"
from:
path: ["^shop/repositories(/.*)?\\.py$", "^shop/db(/.*)?\\.py$"]
to:
path: "^shop/db(/.*)?\\.py$"
allowEmpty: true
- name: db-protected:outside-the-root-packages
comment: "import-linter contract: Only the repositories open database sessions"
from:
pathNot: "^shop(/.*)?\\.py$"
to:
path: "^shop/db(/.*)?\\.py$"
allowEmpty: true
# dependency-cruiser's allowed rules are one allow-list: this one allows every import of a module no protected contract names.
- name: import-linter:unprotected
comment: "import-linter: every import of an unprotected module"
from: {}
to:
pathNot: "^shop/db(/.*)?\\.py$"
allowEmpty: true
allowedSeverity: error
Run over the same repository, the imported rules find the same edge as the rule above:
| Severity | Rule | From | To | Type | Through |
|---|---|---|---|---|---|
| error | not-in-allowed | shop/web/views.py | shop/db/session.py | dependency |
The files are imported/rulebearing.yaml and imported/expected.json.
Try it
The playground holds this page's repository and rule. Run cruises the files in this browser, as rulebearing cruise does in repo/; paste the imported rules into the configuration and run them, or pick another output type.
Everything runs in this browser, with the same library crates as the rulebearing binary: nothing you paste, choose or drop is uploaded or sent anywhere. The configuration is YAML or JSON. A JavaScript configuration is not accepted: the browser build has no JavaScript engine (ADR-0067). An extends of a file cannot be followed; the bundled presets resolve as on the command line. Errors are the module's own words, shown as they come.
- A cruise result: paste the JSON that
rulebearing cruise -T json(or dependency-cruiser) wrote, and Run evaluates the configuration's rules over it, asrulebearing cruise --graph graph.json -T <output type>does. This is the way in for .NET: assemblies are not accepted, since reading them needs a build a browser cannot run, so paste the cruise result the command line wrote for the solution. - Source files: choose a folder or files, or drop them, holding TypeScript and JavaScript or Python, and Run extracts and evaluates them, as
rulebearing cruise -T <output type>does in that folder. Paths are taken from the chosen folder. The language is the one picked, or, left on Infer from the files, the one with more source files outsidenode_modulesand virtual environments (a tie asks you to pick). Files that are not UTF-8 text, files over 1 MiB, anything past 32 MiB or 20,000 files in all, and version-control folders are skipped, and the note under the pickers names them. A configuration file at the folder's root (rulebearing.yaml,rulebearing.yml,rulebearing.jsonor.dependency-cruiser.json) is loaded into the configuration box. What needs the disk or a process is refused with the reason: the Node sidecar, Yarn Plug'n'Play, a ratchet's budget file, and a path outside the folder. - Explain prints what
rulebearing explain <rule> --graph graph.jsonprints for the named rule over the current cruise result: the pasted one, or thejsoncruise of the chosen files.
Examples:
No files chosen yet.