Skip to main content

max / tagtree

Add README with API reference and usage examples Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-03-22 04:41 UTC
Commit: 8dd3c0c94dfe6f3529c5b79413b3f95db4cddfab
Parent: d589b20
1 file changed, +99 insertions, -0 deletions
A README.md +99
@@ -0,0 +1,99 @@
1 + # tagtree
2 +
3 + Hierarchical dot-notation tag standard for Rust. Validation, parsing, tree operations, SQL helpers, and autocomplete — with zero runtime dependencies.
4 +
5 + ## Tag format
6 +
7 + Tags are lowercase dot-separated strings representing a hierarchy:
8 +
9 + ```
10 + genre.electronic.house
11 + work.meeting.standup
12 + news.tech.rust
13 + ```
14 +
15 + Segments allow lowercase alphanumeric characters and hyphens (`[a-z0-9-]`). No empty segments, no leading/trailing dots, no consecutive dots.
16 +
17 + ## Per-app configuration
18 +
19 + Each consumer defines a `TagConfig` controlling depth, length, and semantic prefix rules:
20 +
21 + ```rust
22 + use tagtree::TagConfig;
23 +
24 + // audiofiles: deep hierarchy, namespace-driven
25 + const AF_TAGS: TagConfig = TagConfig { max_depth: 5, max_length: 100, semantic_depth: 1 };
26 +
27 + // goingson: shallow tags, no required prefix
28 + const GO_TAGS: TagConfig = TagConfig { max_depth: 3, max_length: 60, semantic_depth: 0 };
29 + ```
30 +
31 + - `max_depth` — maximum number of segments
32 + - `max_length` — maximum character length of the entire tag
33 + - `semantic_depth` — number of leading segments that carry dispatch meaning (0 = free-form)
34 +
35 + ## API
36 +
37 + ### Validation
38 +
39 + - `validate_with(tag, config)` — validate against a `TagConfig`
40 + - `validate(tag)` — validate with defaults (depth 5, length 100, no semantic prefix)
41 +
42 + ### Parsing
43 +
44 + - `parent(tag)` — `"a.b.c"` -> `Some("a.b")`
45 + - `leaf(tag)` — `"a.b.c"` -> `"c"`
46 + - `depth(tag)` — `"a.b.c"` -> `3`
47 + - `segment(tag, i)` — extract segment by index
48 + - `prefix_at_depth(tag, n)` — first `n` segments
49 + - `ancestors(tag)` — `"a.b.c"` -> `["a", "a.b"]`
50 +
51 + ### Tree operations
52 +
53 + - `is_ancestor_of(a, b)` — true if `a` is a prefix of `b`
54 + - `common_ancestor(a, b)` — longest shared prefix
55 + - `children_at_prefix(prefix, tags)` — direct children one level below prefix
56 + - `subtree(prefix, tags)` — all descendants of prefix
57 + - `rename_prefix(old, new, tag)` — swap a tag's prefix
58 +
59 + ### Semantic splitting
60 +
61 + - `semantic_prefix(tag, depth)` — namespace portion (`"genre.rock"` with depth 1 -> `"genre"`)
62 + - `free_suffix(tag, depth)` — value portion (`"genre.rock"` with depth 1 -> `"rock"`)
63 +
64 + ### SQL helpers
65 +
66 + - `escape_like(s)` — escape `%`, `_`, `\` for safe embedding in `LIKE` patterns
67 + - `like_descendant_pattern(prefix)` — build `prefix.%` pattern for hierarchy queries
68 +
69 + Works identically on SQLite and PostgreSQL.
70 +
71 + ### TagIndex (autocomplete)
72 +
73 + In-memory sorted index with binary-search lookup and two-tier suggestion:
74 +
75 + ```rust
76 + use tagtree::TagIndex;
77 +
78 + let index = TagIndex::new(vec![
79 + "genre.electronic.house".into(),
80 + "genre.electronic.techno".into(),
81 + "genre.rock.punk".into(),
82 + ]);
83 +
84 + // Path prefix: "genre.electronic" matches descendants
85 + let results = index.suggest("genre.electronic", 10);
86 + assert_eq!(results, vec!["genre.electronic.house", "genre.electronic.techno"]);
87 +
88 + // Segment prefix: "gen" matches by segment start
89 + let results = index.suggest("gen", 10);
90 + assert!(results.contains(&"genre.electronic.house"));
91 + ```
92 +
93 + ## Dependencies
94 +
95 + Zero runtime dependencies. `std`-only. Criterion is a dev-dependency for benchmarks.
96 +
97 + ## License
98 +
99 + PolyForm Noncommercial 1.0.0