Rulebearing Playground Coverage Schemas GitHub

The domain does no I/O

A domain that reads the database or calls the network cannot be tested without them, and its rules end up spread between the domain and the storage code. import-linter writes "these modules may not import those" as a forbidden contract. In Rulebearing it is a forbidden rule with reachable: true, so that a chain through another module counts as well as a direct import, as it does in import-linter unless the contract sets allow_indirect_imports.

The rule

Nothing under shop/domain/ may reach shop/db/ or shop/http/. shop/app.py, which wires the domain to storage and the network, is not under shop/domain/ and may import all three.

rulebearing.yaml
$schema: https://benbahrenburg.github.io/rulebearing/schema/config-v1.json
languages:
  python: {}
rules:
  dependencies:
    forbidden:
      - name: domain-does-no-io
        comment: "The domain is tested without a database or a network, so it reaches neither, directly or through a chain. plan:0004-wave-4-reach"
        fix: "Load the data in the caller (shop/app.py) and pass the values into the domain function."
        severity: error
        from: { path: "^shop/domain/" }
        to: { path: "^shop/(db|http)/", reachable: true }
        examples:
          forbidden:
            - "shop/domain/invoice.py -> shop/db/session.py"
            - "shop/domain/money.py -> shop/http/client.py"
          allowed:
            - "shop/domain/invoice.py -> shop/domain/money.py"
            - "shop/app.py -> shop/db/session.py"

The file is rulebearing.yaml. Its examples are the edges rulebearing test checks the rule against. Without reachable: true the rule would check direct imports only, which is import-linter's allow_indirect_imports = True.

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": "domain-does-no-io",
      "comment": "The domain is tested without a database or a network, so it reaches neither, directly or through a chain. plan:0004-wave-4-reach",
      "to": {
        "path": "^shop/(db|http)/",
        "reachable": true
      },
      "severity": "error",
      "from": {
        "path": "^shop/domain/"
      }
    }
  ]
}
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[domain-does-no-io].fix: a Rulebearing rule field
  forbidden[domain-does-no-io].examples: a Rulebearing rule field

The files are converted/.dependency-cruiser.json and converted/dropped.txt.

The failing edge

In repo/, shop/domain/invoice.py has a helper that loads an invoice's lines from the database before totalling them:

SeverityRuleFromToTypeThrough
errordomain-does-no-ioshop/domain/invoice.pyshop/db/session.pyreachabilityshop/db/session.py

The fix

Load in the caller and pass the values in. Delete total_of from the domain, and let shop/app.py do the loading:

# shop/app.py
from shop.db import session
from shop.domain import invoice
from shop.http import client


def run(invoice_id: str) -> None:
    total = invoice.total(session.load_lines(invoice_id))
    session.save("invoice", total)
    client.post("/invoices", total)

From an import-linter contract

The same question as import-linter's forbidden contract, in repo/.importlinter:

.importlinter
[importlinter]
root_package = shop

[importlinter:contract:domain-no-io]
name = The domain does no I/O
type = forbidden
source_modules =
    shop.domain
forbidden_modules =
    shop.db
    shop.http

rulebearing import import-linter translates it into one reachable rule over both forbidden modules, with the graph.chainsThrough that keeps chains inside the root package (ADR-0038):

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:
    forbidden:
      # import-linter contract `domain-no-io` (forbidden): The domain does no I/O
      #   forbidden_modules = shop.db, shop.http
      #   source_modules = shop.domain
      - name: domain-no-io
        comment: "import-linter contract: The domain does no I/O"
        severity: error
        from:
          path: "^shop/domain(/.*)?\\.py$"
        to:
          path: ["^shop/db(/.*)?\\.py$", "^shop/http(/.*)?\\.py$"]
          reachable: true
        graph:
          chainsThrough: "^shop(/.*)?\\.py$"

Run over the same repository, the imported rule finds the same edge as the rule above:

SeverityRuleFromToTypeThrough
errordomain-no-ioshop/domain/invoice.pyshop/db/session.pyreachabilityshop/db/session.py

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 rule into the configuration and run it, 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, as rulebearing 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 outside node_modules and 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.json or .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.json prints for the named rule over the current cruise result: the pasted one, or the json cruise of the chosen files.

Examples:

Loading the rulebearing module.