Skip to main content

A tiny, paste-friendly directive mini-language: ACTION:[<KIND>]NAME[@PATH][=NOTE].

Project description

directiva

Write the rule on one line. Match anything. Keep the reason.

Rust 2024 MSRV 1.85 License: MIT grammar: context-free Types: pyright

A tiny, paste-friendly directive mini-language — one rule per line, the kind every linter, build tool and policy gate keeps reinventing:

ACTION : [<KIND>] NAME [@PATH] [=NOTE]

Verb the things kinded <KIND>, named NAME, located @PATH — because NOTE. It fits in a shell flag, a CI config, or a comment. The crate knows nothing about your domain: you bring the verbs and the things they act on; directiva brings the parser, the glob matcher, the matching rules, and a severity ladder.

use directiva::{parse, Pattern, Target};

let d = parse("de-escalate:<method>get_*@*/tests/*=test fixtures").unwrap();
// d.action == "de-escalate", d.kind == Some("method"), d.name == "get_*", …
mytool ./src \
  -D 'suppress:<function>__repr__@*=dataclass boilerplate' \
  -D 'escalate:<class>*Service@*/core/*=core services must be unique' \
  -D @ci/directives.txt
directiva = "0.2"   # edition 2024, MSRV 1.85

The idea

The shape selector → verb → reason has been reinvented a hundred times — .gitattributes (glob attr=value), CODEOWNERS (glob @team), .dockerignore, Checkstyle's suppressions.xml, rsync's --filter='- *.tmp', Snort rules (alert … (msg:…)). Each is a single-domain version welded into one tool. directiva is the domain-agnostic substrate they each rebuild — so you can use one grammar, one parser, and one consistent UX across all of your tools.

Two things make the shape worth extracting:

  • The reason travels with the rule. =NOTE is a first-class field. Most suppression systems (# noqa, baseline files) throw the why away; here it ships next to the match, ready for the next reviewer.
  • The verbs are yours. The crate has no built-in actions. suppress / de-escalate are defined by your code, exactly like a custom quarantine / allow / promote. The action token is opaque; you map it to whatever you like.

The shape

de-escalate : <method> get_*  @ */tests/*  = test fixtures
└────┬─────┘  └──┬───┘ └─┬──┘   └───┬────┘   └─────┬──────┘
  ACTION       KIND    NAME       PATH            NOTE
  (your verb,  (exact  (glob,     (glob, any-of   (free text;
   opaque)     filter; any-of     scopes)          the "why")
               <*>=any) names)
field required matched against sigil
ACTION yes resolved by your Action::from_token (or kept as a String) ends at first :
<KIND> no exact equality with the target's qualifier (<*>/omitted = any) <…>
NAME yes glob, any of the target's names
@PATH no glob, any of the target's scopes @…
=NOTE no not matched — surfaced beside the result =…

The grammar is context-free (the angle-bracketed <KIND> is what buys that — no vocabulary needed to parse, and a NAME can freely contain :, so Rust paths like Foo::bar are just names).


What it does (Rust)

You implement [Target] for whatever a directive should select. Matching is pushed into the target, so it's allocation-free and you decide what counts as a "name" (aliases, a joined form, …):

use directiva::{parse, Pattern, Target};

struct Item { kind: &'static str, name: &'static str, file: &'static str }

impl Target for Item {
    fn qualifier(&self) -> Option<&str>           { Some(self.kind) }
    fn matches_name(&self, p: &Pattern) -> bool   { p.matches(self.name) }
    fn matches_scope(&self, p: &Pattern) -> bool  { p.matches(self.file) }
}

let d = parse("suppress:<function>get_*@*/tests/*").unwrap();
let hit  = Item { kind: "function", name: "get_user", file: "src/tests/api.rs" };
let miss = Item { kind: "function", name: "get_user", file: "src/api.rs" };
assert!(d.matches(&hit));
assert!(!d.matches(&miss));   // @PATH glob doesn't match

Closed-set actions catch typos; the default String action is the open set:

use directiva::{parse_as, Action};

enum Verb { Suppress, Quarantine }
impl Action for Verb {
    fn from_token(t: &str) -> Option<Self> {
        match t { "suppress" => Some(Verb::Suppress), "quarantine" => Some(Verb::Quarantine), _ => None }
    }
}
assert!(parse_as::<Verb>("supress:foo").is_err());   // typo → UnknownAction, not a silent no-op

Severity is a decoupled monoid

[Ladder<S>] is a standalone, saturating, order-independent ladder over any ordered set — it references neither directives nor actions. Linter severities are one use; a markup workflow is another:

use directiva::Ladder;

let sev = Ladder::new(vec!["error", "warning", "info"]);   // most-severe first
assert_eq!(sev.step(&"error", 2), "info");                  // saturating
assert_eq!(sev.step(&"info", -5), "error");                 // clamps

let flow = Ladder::new(vec!["draft", "review", "final"]);
assert_eq!(flow.step(&"draft", 1), "review");               // not just severities

The CLI use case (-D)

The headline use is many -D flags. directiva::source ingests them with no clap dependency — you wire one call into your own arg parser. One flag, two forms:

mytool -D 'suppress:<function>spawn@*/vendor/*=vendored copy'   # an inline directive
mytool -D @rules.txt                                            # a directive file (one per line)
mytool -D @-                                                    # …or read them from stdin
// for each -D value clap collected:
let directives = directiva::source::cli::expand_all::<directiva::lint::LintAction, _, _>(values)?;

Files get # comments, blank-line skipping, CRLF/BOM tolerance, and line-numbered parse errors. Multiple -D (inline and/or @file) are pure concatenation — no dedup, so a duplicate honestly compounds; that's the caller's call, not magic.

⚠️ Quote your -D values. *, {…}, […] and ! are all shell metacharacters — unquoted, the shell mangles your directive before the tool sees it. Always single-quote: -D 'suppress:*@*/{test,tests}/*'.


The lint pack — batteries included

directiva::lint is a ready-made instantiation for the classic lint case: a concrete LintAction vocabulary (suppress / de-escalate / escalate / note / set), a Severity ladder, and a fold combine policy.

use directiva::lint::{self, LintAction, Severity};
use directiva::source::cli;

let dirs: Vec<_> = cli::expand_all::<LintAction, _, _>([
    "de-escalate:<method>get_*@*/tests/*=test fixtures",
    "suppress:<function>spawn@*/vendor/*=vendored copy",
    "set:max-name-group=256",
])?.into_iter().map(|s| s.directive).collect();

let outcome = lint::fold(Severity::Error, &finding, &dirs);
// outcome.severity  (stepped + clamped)   outcome.dropped (suppressed?)   outcome.notes
let settings = lint::extract_settings(&dirs);   // [("max-name-group", "256")] — you apply them

Notes accumulate from every match; escalate/de-escalate steps sum then clamp; suppress drops; set is config (skipped by the fold). It's one pack — a tool with a different vocabulary (allow/deny, draft<review<final) defines its own beside it.


Use it in any domain

Same grammar, your verbs — a few sketches:

# access control          (KIND = resource type, PATH = tenant)
-D 'deny:<route>/admin/*@*=staff only'    -D 'allow:<route>/public/*'

# CI / flaky tests
-D 'quarantine:test_checkout_*@*/e2e/*=JIRA-1234, flaky on CI'

# feature flags / rollout
-D 'canary:checkout-v2@*/eu/*=10% EU'

# alert routing           (ladder: log < ticket < page)
-D 'silence:<alert>DiskWarn*@*/staging/*=staging noise'   -D 'escalate:<alert>Oom*@*/prod/*'

# doc workflow            (ladder: draft < review < final)
-D 'promote:chapter-*@drafts/q3/*=ready for review'

# data pipeline / PII
-D 'mask:<column>*_ssn@*/users/*=PII'

The grammar

A clean context-free grammar; the angle-bracketed <KIND> removes the only ambiguity. NAME/PATH are globs; =NOTE is whatever follows the first unescaped = (so it may contain : @ =).

directive ::= action ":" body
body      ::= [ "<" kind ">" ] name [ "@" path ] [ "=" note ]

Glob: * (any run, including / — not path-segment-aware), ? (one byte), {a,b} alternation (one level), [a-z]/[!neg] shell-style char-classes, and one universal \ escape. Matching is full-anchor (foo matches only foo, not foobar — use *foo*) and linear-time (no catastrophic backtracking on untrusted patterns). Compilation is bounded too: brace groups cross-multiply ({a,b}{c,d} → 4), so the expander caps the product at MAX_BRACE_ALTS (a public core::glob const) — a short {a,b}×30 "brace bomb" would otherwise be 2³⁰ sub-patterns; past the cap the braces degrade to literals. No OOM on hostile input.


Architecture

Three layers; everything but core is feature-gated but on by default.

module role
directiva::core the engine — glob (Pattern), parse/parse_as, Directive<A = String> + the Action trait, the Target trait + matching, and the decoupled Ladder<S>. Domain-agnostic, always on.
directiva::source ingestion — the -D overload (cli::expand) and the line-based file parser. No clap dependency. (feature source)
directiva::lint the standard batteries — LintAction, Severity, fold, extract_settings. One instantiation, not part of the engine. (feature lint)

A minimal embedder takes just core (default-features = false).


Python package

The same grammar from Python — parse, match, glob, expand -D values, and the lint fold:

import directiva
from directiva import lint

d = directiva.parse("de-escalate:<method>get_*@*/tests/*=test fixtures")
d.action, d.kind, d.name, d.note     # ('de-escalate', 'method', 'get_*', 'test fixtures')

d.matches(["get_user"], qualifier="method", scopes=["src/tests/api.py"])   # True

directiva.glob_match("*/{test,tests}/*", "pkg/tests/x.py")                 # True
directiva.expand("@ci/rules.txt")                                          # list[Directive]

lint.fold(lint.ERROR, [d], ["get_user"], qualifier="method", scopes=["src/tests/x.py"])
# Outcome(severity='warning', dropped=False, notes=['test fixtures'])

The package is typed (py.typed + stubs; Severity is a Literal), and gated behind the python cargo feature so the pure-Rust crate keeps zero Python dependency by default. Built with maturin (abi3 — one wheel per platform works on CPython ≥ 3.9).

No PyPI. Two ways to install:

1. Prebuilt wheel — no Rust toolchain. Grab the wheel for your platform from the Releases page and install it by URL:

pip install https://github.com/prostomarkeloff/directiva/releases/download/v0.2.0/directiva-0.2.0-cp39-abi3-macosx_11_0_arm64.whl

2. From source — needs a Rust toolchain (pip drives maturin automatically):

pip install git+https://github.com/prostomarkeloff/directiva

Build locally into a venv with maturin develop --features python.


Correctness

The grammar's corner cases (note eats :/@/=, the ::-in-names case, char-class edges, escaping across layers, severity clamping, file conventions) are pinned by the test suite, and the engine's safety invariants (the parser never panics, glob compilation stays bounded, literals are full-anchored) are exercised by proptest:

cargo test                 # 64 tests: core + source + lint + property + integration + doctest
cargo clippy --all-targets

The parser and glob engine also carry cargo-fuzz harnesses — totality and the brace-bomb cap are asserted as fuzz invariants:

cargo +nightly fuzz run parse      # the parser is total: Result, never a panic
cargo +nightly fuzz run glob       # compile stays ≤ MAX_BRACE_ALTS; matches always terminates

One line. Any tool. The reason, attached.

Made with ⚡ by @prostomarkeloff

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

directiva-0.2.0.tar.gz (44.0 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

directiva-0.2.0-cp39-abi3-win_amd64.whl (194.7 kB view details)

Uploaded CPython 3.9+Windows x86-64

directiva-0.2.0-cp39-abi3-musllinux_1_2_x86_64.whl (544.1 kB view details)

Uploaded CPython 3.9+musllinux: musl 1.2+ x86-64

directiva-0.2.0-cp39-abi3-musllinux_1_2_aarch64.whl (512.1 kB view details)

Uploaded CPython 3.9+musllinux: musl 1.2+ ARM64

directiva-0.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (339.4 kB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ x86-64

directiva-0.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (337.0 kB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ ARM64

directiva-0.2.0-cp39-abi3-macosx_11_0_arm64.whl (300.7 kB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

directiva-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl (305.4 kB view details)

Uploaded CPython 3.9+macOS 10.12+ x86-64

File details

Details for the file directiva-0.2.0.tar.gz.

File metadata

  • Download URL: directiva-0.2.0.tar.gz
  • Upload date:
  • Size: 44.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.1

File hashes

Hashes for directiva-0.2.0.tar.gz
Algorithm Hash digest
SHA256 cf7f61c0648f40af980f0f2d34d305d84c3ab2a10f87e258494feb2d1a41d28c
MD5 e5468521a94bdb17bc927bf5b851c1b9
BLAKE2b-256 c4f7e58df9114e7d53985c9d8675f4c9a4fd9c508a0863273e9f65aed30ef863

See more details on using hashes here.

File details

Details for the file directiva-0.2.0-cp39-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for directiva-0.2.0-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 bd4a500c638f11f50770b5e81229674aeb5ce941acae976acc23127462daf861
MD5 1156cd24ff734fc0a280566efad2f6c5
BLAKE2b-256 d3a4f992aa67f12ccd7458c95a5a2faddecebd1a725e84b74db951f872abba7d

See more details on using hashes here.

File details

Details for the file directiva-0.2.0-cp39-abi3-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for directiva-0.2.0-cp39-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 066f085a428a18874ccdadfffcdbbfd551c6bafedbd7d7d0ba67c1f71bef5f16
MD5 10d615b95256bd2976bb049eb0f93882
BLAKE2b-256 5702f6b9457e5c12f76a9941c0ca4220165401e2d71f66b42a3d4c5bf86ac443

See more details on using hashes here.

File details

Details for the file directiva-0.2.0-cp39-abi3-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for directiva-0.2.0-cp39-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 96643bcafd8636d7951e0e058c5d8e765f4067f3cfa29e2fc162679519928707
MD5 8425e9851c04e499d5397540a9a2bdd4
BLAKE2b-256 0c6faeba2887171054f1d8ca3b34d15020f54fb88b10296366e021b3d030a1fd

See more details on using hashes here.

File details

Details for the file directiva-0.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for directiva-0.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 edf91c0711b802bf1306caaff216d296027967d53cc1cb68ef1ba60babcadbbf
MD5 74a9ab9edf8225d7ce013137be66eb53
BLAKE2b-256 f21a263915a5b03cfcb9cbc4c68517b72e6b297b90ba6409a6ce95946d1d8c0d

See more details on using hashes here.

File details

Details for the file directiva-0.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for directiva-0.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 ad3e9e41becf063d519b176f854c18dcd459d8a7282a54b04803c760806538ab
MD5 f52e7aae8927434b3ba83f88d8914fea
BLAKE2b-256 6560ae84d4ccfaf146bcbeb6108fac645d957b27d0b9aa94301974cb14843c94

See more details on using hashes here.

File details

Details for the file directiva-0.2.0-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for directiva-0.2.0-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 dbe702bcafc81c64f9ee949ddeffdf075a111310f20162beabf4becd2522c366
MD5 03e868abfc5fd373428676eacea16b48
BLAKE2b-256 2d769b489b5b22f52d64ac95445be45b6d6be991093f80e8961c61146b6d774a

See more details on using hashes here.

File details

Details for the file directiva-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for directiva-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 136cb8276f30d0e8d62bd6b28b9061b16f0bc1690d753588abf40f6603a13604
MD5 055f5069651e20786971bbc7a09cd1b4
BLAKE2b-256 d3c3e0f176c2fed6fbcd21e290a81e4f055b1ab8dcb1c8ec2c32f4a9844cd855

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page