Skip to main content

max / tagtree

git clone https://makenot.work/git/max/tagtree.git git clone git@ssh.makenot.work:max/tagtree.git
Name Size
/ benches/
/ docs/
/ src/
· .gitignore 19 B
· Cargo.lock 14.1 KB
· Cargo.toml 518 B
· LICENSE 4.5 KB
· README.md 3.0 KB

README

tagtree

Hierarchical dot-notation tag standard for Rust. Validation, parsing, tree operations, SQL helpers, and autocomplete — with zero runtime dependencies.

Tag format

Tags are lowercase dot-separated strings representing a hierarchy:

genre.electronic.house
work.meeting.standup
news.tech.rust

Segments allow lowercase alphanumeric characters and hyphens ([a-z0-9-]). No empty segments, no leading/trailing dots, no consecutive dots.

Per-app configuration

Each consumer defines a TagConfig controlling depth, length, and semantic prefix rules:

use tagtree::TagConfig;

// audiofiles: deep hierarchy, namespace-driven
const AF_TAGS: TagConfig = TagConfig { max_depth: 5, max_length: 100, semantic_depth: 1 };

// goingson: shallow tags, no required prefix
const GO_TAGS: TagConfig = TagConfig { max_depth: 3, max_length: 60, semantic_depth: 0 };
  • max_depth — maximum number of segments
  • max_length — maximum character length of the entire tag
  • semantic_depth — number of leading segments that carry dispatch meaning (0 = free-form)

API

Validation

  • validate_with(tag, config) — validate against a TagConfig
  • validate(tag) — validate with defaults (depth 5, length 100, no semantic prefix)

Parsing

  • parent(tag)"a.b.c" -> Some("a.b")
  • leaf(tag)"a.b.c" -> "c"
  • depth(tag)"a.b.c" -> 3
  • segment(tag, i) — extract segment by index
  • prefix_at_depth(tag, n) — first n segments
  • ancestors(tag)"a.b.c" -> ["a", "a.b"]

Tree operations

  • is_ancestor_of(a, b) — true if a is a prefix of b
  • common_ancestor(a, b) — longest shared prefix
  • children_at_prefix(prefix, tags) — direct children one level below prefix
  • subtree(prefix, tags) — all descendants of prefix
  • rename_prefix(old, new, tag) — swap a tag’s prefix

Semantic splitting

  • semantic_prefix(tag, depth) — namespace portion ("genre.rock" with depth 1 -> "genre")
  • free_suffix(tag, depth) — value portion ("genre.rock" with depth 1 -> "rock")

SQL helpers

  • escape_like(s) — escape %, _, \ for safe embedding in LIKE patterns
  • like_descendant_pattern(prefix) — build prefix.% pattern for hierarchy queries

Works identically on SQLite and PostgreSQL.

TagIndex (autocomplete)

In-memory sorted index with binary-search lookup and two-tier suggestion:

use tagtree::TagIndex;

let index = TagIndex::new(vec![
    "genre.electronic.house".into(),
    "genre.electronic.techno".into(),
    "genre.rock.punk".into(),
]);

// Path prefix: "genre.electronic" matches descendants
let results = index.suggest("genre.electronic", 10);
assert_eq!(results, vec!["genre.electronic.house", "genre.electronic.techno"]);

// Segment prefix: "gen" matches by segment start
let results = index.suggest("gen", 10);
assert!(results.contains(&"genre.electronic.house"));

Dependencies

Zero runtime dependencies. std-only. Criterion is a dev-dependency for benchmarks.

License

PolyForm Noncommercial 1.0.0