Rulebearing Playground Coverage Schemas GitHub

Packages do not import each other

Orders, billing and shipping are owned and changed separately, so none of them may depend on another; what they share lives in a fourth package they all may use. import-linter writes this as an independence contract. In Rulebearing it is one forbidden rule with a capture, and reachable: true so that a chain through a shared module counts too, as it does in import-linter.

The rule

from.path captures which of the three packages the importer is in, and to.pathNot lets it reach only its own. shop/common/ is in none of the three, so every package may use it.

rulebearing.yaml
$schema: https://benbahrenburg.github.io/rulebearing/schema/config-v1.json
languages:
  python: {}
rules:
  dependencies:
    forbidden:
      - name: orders-billing-shipping-are-independent
        comment: "Each of the three packages is changed, tested and owned on its own, so none reaches another, directly or through a chain. plan:0004-wave-4-reach"
        fix: "Move what two of the packages share into shop/common/, or let the caller pass one package's result to the other."
        severity: error
        from: { path: "^shop/(orders|billing|shipping)/" }
        to: { path: "^shop/(orders|billing|shipping)/", pathNot: "^shop/$1/", reachable: true }
        examples:
          forbidden:
            - "shop/orders/place.py -> shop/billing/invoice.py"
            - "shop/shipping/label.py -> shop/orders/place.py"
          allowed:
            - "shop/orders/place.py -> shop/common/ids.py"
            - "shop/billing/invoice.py -> shop/billing/__init__.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": "orders-billing-shipping-are-independent",
      "comment": "Each of the three packages is changed, tested and owned on its own, so none reaches another, directly or through a chain. plan:0004-wave-4-reach",
      "to": {
        "path": "^shop/(orders|billing|shipping)/",
        "pathNot": "^shop/$1/",
        "reachable": true
      },
      "severity": "error",
      "from": {
        "path": "^shop/(orders|billing|shipping)/"
      }
    }
  ]
}
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[orders-billing-shipping-are-independent].fix: a Rulebearing rule field
  forbidden[orders-billing-shipping-are-independent].examples: a Rulebearing rule field

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

The failing edge

In repo/, placing an order issues its invoice: shop/orders/place.py imports shop.billing.invoice.

SeverityRuleFromToTypeThrough
errororders-billing-shipping-are-independentshop/orders/place.pyshop/billing/invoice.pyreachabilityshop/billing/invoice.py

The fix

Let the caller that needs both, outside the three packages, call each in turn, and keep place to taking the order:

# shop/orders/place.py
from shop.common import ids


def place(total_cents: int) -> str:
    return ids.new_id("order")


# shop/checkout.py
from shop.billing import invoice
from shop.orders import place


def checkout(total_cents: int) -> str:
    order_id = place.place(total_cents)
    invoice.issue(order_id, total_cents)
    return order_id

From an import-linter contract

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

.importlinter
[importlinter]
root_package = shop

[importlinter:contract:independence]
name = Orders, billing and shipping do not import each other
type = independence
modules =
    shop.orders
    shop.billing
    shop.shipping

rulebearing import import-linter translates it into one reachable rule per ordered pair of packages, each 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 `independence` (independence): Orders, billing and shipping do not import each other
      #   modules = shop.orders, shop.billing, shop.shipping
      - name: independence:shop.orders-to-shop.billing
        comment: "import-linter contract: Orders, billing and shipping do not import each other"
        severity: error
        from:
          path: "^shop/orders(/.*)?\\.py$"
        to:
          path: "^shop/billing(/.*)?\\.py$"
          reachable: true
        graph:
          chainsThrough: "^shop(/.*)?\\.py$"
      - name: independence:shop.orders-to-shop.shipping
        comment: "import-linter contract: Orders, billing and shipping do not import each other"
        severity: error
        from:
          path: "^shop/orders(/.*)?\\.py$"
        to:
          path: "^shop/shipping(/.*)?\\.py$"
          reachable: true
        graph:
          chainsThrough: "^shop(/.*)?\\.py$"
      - name: independence:shop.billing-to-shop.orders
        comment: "import-linter contract: Orders, billing and shipping do not import each other"
        severity: error
        from:
          path: "^shop/billing(/.*)?\\.py$"
        to:
          path: "^shop/orders(/.*)?\\.py$"
          reachable: true
        graph:
          chainsThrough: "^shop(/.*)?\\.py$"
      - name: independence:shop.billing-to-shop.shipping
        comment: "import-linter contract: Orders, billing and shipping do not import each other"
        severity: error
        from:
          path: "^shop/billing(/.*)?\\.py$"
        to:
          path: "^shop/shipping(/.*)?\\.py$"
          reachable: true
        graph:
          chainsThrough: "^shop(/.*)?\\.py$"
      - name: independence:shop.shipping-to-shop.orders
        comment: "import-linter contract: Orders, billing and shipping do not import each other"
        severity: error
        from:
          path: "^shop/shipping(/.*)?\\.py$"
        to:
          path: "^shop/orders(/.*)?\\.py$"
          reachable: true
        graph:
          chainsThrough: "^shop(/.*)?\\.py$"
      - name: independence:shop.shipping-to-shop.billing
        comment: "import-linter contract: Orders, billing and shipping do not import each other"
        severity: error
        from:
          path: "^shop/shipping(/.*)?\\.py$"
        to:
          path: "^shop/billing(/.*)?\\.py$"
          reachable: true
        graph:
          chainsThrough: "^shop(/.*)?\\.py$"

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

SeverityRuleFromToTypeThrough
errorindependence:shop.orders-to-shop.billingshop/orders/place.pyshop/billing/invoice.pyreachabilityshop/billing/invoice.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 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, 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.