You are a constitutional council ranking individual git commits for ownership allocation. Compare these two commits. Decide which contributed more lasting value to the project. Judge substance, not spectacle: - Prefer correct, lasting design and real bugfixes over churn, formatting, renames, or generated noise. - Prefer clarity and necessity over sheer line count. A small precise change can beat a large diffuse one. - Do not favor a side merely because its patch is longer or noisier. - Weight what the change does for the project, not the contributor's name. Return ONLY a JSON object: {"winner": "A" or "B", "ratio": "N:M", "explanation": "..."} The explanation must cite concrete differences in the patches (1-3 sentences). Side A — contributor: tommy-mor Side A — commit message: [aa0b175c] Enforce vote ratio constraints: both sides ≥ 1, max 100. Zero on either side produces no valid graph edge; ratios above 100 add no meaningful signal. Enforce in the DSL parser, browser POST handler, and reducer guard. Update browser pool test to use 99:1 instead of 100:0. Add unit and integration regression tests. Co-Authored-By: Claude Sonnet 4.6 Side A — unified diff (full patch): diff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs index b79efdb4d52bd445a67f38cbfd61d3507d2b3014..bc8a0130b434cc7880a4bf16eb9237080c4aa383 100644 --- a/server/src/api/ui_html.rs +++ b/server/src/api/ui_html.rs @@ -243,11 +243,23 @@ async fn dispatch_ui_action( let pool_id = pool.as_deref().and_then(|p| { crate::path_types::ItemId::parse(p.trim()).map(|i| i.normalized_storage()) }); - let mut rl = ratio_left.trim().parse::().unwrap_or(0).max(0); - let mut rr = ratio_right.trim().parse::().unwrap_or(0).max(0); - if rl == 0 && rr == 0 { - rl = 1; - rr = 1; + let rl = ratio_left.trim().parse::().unwrap_or(0).max(0); + let rr = ratio_right.trim().parse::().unwrap_or(0).max(0); + if rl == 0 || rr == 0 { + return form_js_error( + err_tgt.as_ref(), + "invalid ratio", + "Both ratio sides must be ≥ 1.", + ) + .into_response(); + } + if rl > 100 || rr > 100 { + return form_js_error( + err_tgt.as_ref(), + "invalid ratio", + "Ratio sides must be ≤ 100.", + ) + .into_response(); } let text = format!( diff --git a/server/src/dsl.rs b/server/src/dsl.rs index faa8aac6616bc6ea2b102d08ae999c01a716ef6e..338feacc579a92bed291867cc0f2f62462737462 100644 --- a/server/src/dsl.rs +++ b/server/src/dsl.rs @@ -545,9 +545,14 @@ fn parse_block_prefixed_statement( let ((ratio_left, ratio_right), k) = parse_comparison_at(s, i) .ok_or_else(|| DslError::Parse(format!("invalid comparison near: {}", &s[i..])))?; - if ratio_left == 0 && ratio_right == 0 { + if ratio_left == 0 || ratio_right == 0 { return Err(DslError::Parse( - "vote ratio 0:0 is invalid; use 1:1 for a tie or omit the vote".to_string(), + "vote ratio sides must be ≥ 1; use 1:1 for a tie or omit the vote".to_string(), + )); + } + if ratio_left > 100 || ratio_right > 100 { + return Err(DslError::Parse( + "vote ratio sides must be ≤ 100".to_string(), )); } i = skip_ws(s, k); @@ -929,11 +934,50 @@ mod tests { let err = parse_full("{tie placeholder}\n~/a 0:0 ~/b").unwrap_err(); let DslError::Parse(msg) = err; assert!( - msg.contains("0:0"), - "expected 0:0 rejection message, got: {msg}" + msg.contains("≥ 1"), + "expected zero-side rejection message, got: {msg}" ); } + #[test] + fn parse_vote_rejects_left_zero_ratio() { + let err = parse_full("{prefer b}\n~/a 0:5 ~/b").unwrap_err(); + let DslError::Parse(msg) = err; + assert!( + msg.contains("≥ 1"), + "expected zero-side rejection message, got: {msg}" + ); + } + + #[test] + fn parse_vote_rejects_right_zero_ratio() { + let err = parse_full("{prefer a}\n~/a 5:0 ~/b").unwrap_err(); + let DslError::Parse(msg) = err; + assert!( + msg.contains("≥ 1"), + "expected zero-side rejection message, got: {msg}" + ); + } + + #[test] + fn parse_vote_rejects_over_max_ratio() { + let err = parse_full("{prefer a strongly}\n~/a 101:1 ~/b").unwrap_err(); + let DslError::Parse(msg) = err; + assert!( + msg.contains("≤ 100"), + "expected max ratio rejection message, got: {msg}" + ); + } + + #[test] + fn parse_vote_accepts_max_ratio() { + let doc = parse_full("{prefer a}\n~/a 100:1 ~/b").unwrap(); + assert!(matches!( + doc.statements.last(), + Some(Stmt::Vote { ratio_left: 100, ratio_right: 1, .. }) + )); + } + #[test] fn parse_full_interleaves_prose() { let input = "hello\n#tag\nworld"; diff --git a/server/src/reducer.rs b/server/src/reducer.rs index e949e8e092297eedd5c20c131f16ef0121f329b3..6841d35cfc9de2389f340a22b8a45acb335e36c3 100644 --- a/server/src/reducer.rs +++ b/server/src/reducer.rs @@ -119,11 +119,11 @@ impl GroupState { let (i, j) = if a_idx < b_idx { (a_idx, b_idx) } else { (b_idx, a_idx) }; self.voted_pairs.insert((i, j)); - let mut w_a = vote.ratio_left.max(0) as f64; - let mut w_b = vote.ratio_right.max(0) as f64; - if w_a == 0.0 && w_b == 0.0 { - w_a = 1.0; - w_b = 1.0; + let w_a = vote.ratio_left as f64; + let w_b = vote.ratio_right as f64; + if w_a == 0.0 || w_b == 0.0 { + // Zero on either side produces no valid edge; drop the vote. + return; } self.add_edge_weight(b_idx, a_idx, w_a); diff --git a/server/tests/basic.rs b/server/tests/basic.rs index 9d83e7a97e2c7494790db17c4b5b30c181705026..cc8c1a0d139f3722ba6ecd13dd001c65be835b67 100644 --- a/server/tests/basic.rs +++ b/server/tests/basic.rs @@ -175,14 +175,14 @@ fn reducer_clamps_score_bounds() { let mut state = ReducerState::default(); state.apply_event(ingest_event( 1, - "@00000000-0000-0000-0000-000000000000:test:local/test\n~/t/a {a}\n~/t/b {b}\n{huge}\n~/t/a 1000:1 ~/t/b\n", + "@00000000-0000-0000-0000-000000000000:test:local/test\n~/t/a {a}\n~/t/b {b}\n{huge}\n~/t/a 100:1 ~/t/b\n", )); state.apply_event(ingest_event( 2, - "@00000000-0000-0000-0000-000000000000:test:local/test\n{huge}\n~/t/a 1:1000 ~/t/b\n", + "@00000000-0000-0000-0000-000000000000:test:local/test\n{huge}\n~/t/a 1:100 ~/t/b\n", )); - assert_eq!(state.public().ranking_group.idx_to_item.len(), 2); // Should still work, scores clamped internally + assert_eq!(state.public().ranking_group.idx_to_item.len(), 2); // Should still work, scores handled internally } // ============================================================================ @@ -513,8 +513,8 @@ fn dsl_parse_rejects_zero_zero_vote_ratio() { .expect_err("0:0 vote must be rejected by the parser"); let slugsocial_server::dsl::DslError::Parse(msg) = err; assert!( - msg.contains("0:0"), - "expected message about invalid 0:0 ratio, got: {msg}" + msg.contains("≥ 1"), + "expected message about invalid zero ratio, got: {msg}" ); let mut state = ReducerState::default(); @@ -533,7 +533,7 @@ fn dsl_parse_rejects_zero_zero_vote_ratio() { #[test] fn reducer_negative_ratio_clamped_to_zero() { let _state = ReducerState::default(); - // GroupState::apply_vote clamps negatives to 0, then 0:0 -> 1:1 + // GroupState::apply_vote clamps negatives to 0; when either side is 0 the vote is dropped. let mut group = GroupState::new(); group.apply_vote(slugsocial_server::reducer::VoteData { ts: 1, @@ -546,12 +546,12 @@ fn reducer_negative_ratio_clamped_to_zero() { delegate: Some("00000000-0000-0000-0000-000000000000:test:local/test".to_string()), thread_tag: "t".to_string(), }); + // Items are registered, but the zero-clamped vote produces no edges. assert_eq!(group.idx_to_item.len(), 2); - // Both edges should exist (negatives clamped to 0, then 0:0 -> 1:1) let a_idx = group.item_to_idx[&item_id("https://slug.social/~/t/a")]; let b_idx = group.item_to_idx[&item_id("https://slug.social/~/t/b")]; - assert!(group.edges.contains_key(&(a_idx, b_idx))); - assert!(group.edges.contains_key(&(b_idx, a_idx))); + assert!(!group.edges.contains_key(&(a_idx, b_idx))); + assert!(!group.edges.contains_key(&(b_idx, a_idx))); } diff --git a/server/tests/integration_ui.rs b/server/tests/integration_ui.rs index 23db7b5672418d5bb0ab7529e05ef1f89e59062a..09c5481f039d59efb617a82bafcc432a186e2d30 100644 --- a/server/tests/integration_ui.rs +++ b/server/tests/integration_ui.rs @@ -256,3 +256,96 @@ async fn test_sse_public_thread_morph_includes_post_body_not_thread_not_found() ); } +fn ui_vote_compare_post_rpc( + room: &str, + thread_tag: &str, + left: &str, + right: &str, + ratio_left: &str, + ratio_right: &str, + explanation: &str, +) -> String { + serde_json::json!({ + "action": "vote_compare_post", + "room": room, + "thread_tag": thread_tag, + "left_item": left, + "right_item": right, + "ratio_left": ratio_left, + "ratio_right": ratio_right, + "explanation": explanation, + "next": "/vote", + }) + .to_string() +} + +#[tokio::test] +async fn test_vote_compare_post_rejects_zero_left_ratio() { + let (addr, _tmp, _log, _handle) = create_test_server().await; + let client = reqwest::Client::new(); + let bearer = test_bearer(); + + let rpc = ui_vote_compare_post_rpc("public", "test-vote", "~/a", "~/b", "0", "5", "prefer b"); + let resp = client + .post(format!("http://{addr}/ui")) + .header("Authorization", format!("Bearer {bearer}")) + .form(&[("__rpc__", rpc.as_str())]) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), reqwest::StatusCode::OK); + let js = resp.text().await.unwrap(); + assert!( + js.contains("invalid ratio") || js.contains("≥ 1"), + "expected zero-ratio rejection, got: {js}" + ); +} + +#[tokio::test] +async fn test_vote_compare_post_rejects_zero_right_ratio() { + let (addr, _tmp, _log, _handle) = create_test_server().await; + let client = reqwest::Client::new(); + let bearer = test_bearer(); + + let rpc = ui_vote_compare_post_rpc("public", "test-vote", "~/a", "~/b", "5", "0", "prefer a"); + let resp = client + .post(format!("http://{addr}/ui")) + .header("Authorization", format!("Bearer {bearer}")) + .form(&[("__rpc__", rpc.as_str())]) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), reqwest::StatusCode::OK); + let js = resp.text().await.unwrap(); + assert!( + js.contains("invalid ratio") || js.contains("≥ 1"), + "expected zero-ratio rejection, got: {js}" + ); +} + +#[tokio::test] +async fn test_vote_compare_post_rejects_over_max_ratio() { + let (addr, _tmp, _log, _handle) = create_test_server().await; + let client = reqwest::Client::new(); + let bearer = test_bearer(); + + let rpc = + ui_vote_compare_post_rpc("public", "test-vote", "~/a", "~/b", "101", "1", "prefer a"); + let resp = client + .post(format!("http://{addr}/ui")) + .header("Authorization", format!("Bearer {bearer}")) + .form(&[("__rpc__", rpc.as_str())]) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), reqwest::StatusCode::OK); + let js = resp.text().await.unwrap(); + assert!( + js.contains("invalid ratio") || js.contains("≤ 100"), + "expected over-max ratio rejection, got: {js}" + ); +} + diff --git a/test/browser_vote_pool.clj b/test/browser_vote_pool.clj index 23d0bd80b02bd8b1b48853454bed02793296550e..738608801027d1e662660b5d6c830b4ccd07c434 100644 --- a/test/browser_vote_pool.clj +++ b/test/browser_vote_pool.clj @@ -38,8 +38,8 @@ ;; Set the hidden ratio inputs so the alphabetically-earlier item wins. (defn- set-ratio! [pg left-text right-text] (let [[rl rr] (if (neg? (compare (leaf left-text) (leaf right-text))) - [100 0] ; left is earlier → prefer left - [0 100])] ; right is earlier → prefer right + [99 1] ; left is earlier → prefer left + [1 99])] ; right is earlier → prefer right (page/evaluate pg (str "document.getElementById('vote-ratio-left').value='" rl "'")) (page/evaluate pg (str "document.getElementById('vote-ratio-right').value='" rr "'")))) Side B — contributor: tommy-mor Side B — commit message: [aa64d2f6] nice Side B — unified diff (full patch): diff --git a/server/src/api/helpers.rs b/server/src/api/helpers.rs index 8cd23a02fa5219d6aa375e766e6b2bd2c7bb7dfb..6a78c7346e3505b54f2587bbca402379bb929f81 100644 --- a/server/src/api/helpers.rs +++ b/server/src/api/helpers.rs @@ -31,13 +31,13 @@ pub fn now_ms() -> i64 { t.as_millis() as i64 } -/// Resolve DSL/user input to a stored canonical item id. +/// Resolve DSL/user input to a stored [`ItemId`]. pub fn resolve_item(item: &str) -> Result { - let canonical = canonicalize_item(item); - if canonical.is_empty() { + let wire = canonicalize_item(item); + if wire.is_empty() { return Err(format!("empty item path: `{}`", item)); } - ItemId::parse(&canonical).ok_or_else(|| format!("invalid item path: `{}`", item)) + ItemId::parse(&wire).ok_or_else(|| format!("invalid item path: `{}`", item)) } pub fn parse_parent_specs(parent: Option<&String>) -> Vec { @@ -95,7 +95,7 @@ pub fn paginate_rankings( (out_components, out_unranked) } -pub fn pick_random_distinct_canonical(items: &[ItemId]) -> Option<(ItemId, ItemId)> { +pub fn pick_random_distinct_item_pair(items: &[ItemId]) -> Option<(ItemId, ItemId)> { use rand::seq::SliceRandom; if items.len() < 2 { return None; diff --git a/server/src/api/mod.rs b/server/src/api/mod.rs index 1528b3ff69e8d634158ee56f6b658752a3b90e94..9f3c1cc21c2ae157c024a447980a97e190c8f066 100644 --- a/server/src/api/mod.rs +++ b/server/src/api/mod.rs @@ -27,7 +27,7 @@ pub use auth::{ pub use helpers::{ api_error, compute_connectivity_stats, is_pair_voted, now_ms, paginate_rankings, - parse_parent_specs, pick_random_distinct_canonical, resolve_item, sha256_hex, vote_touches_path, + parse_parent_specs, pick_random_distinct_item_pair, resolve_item, sha256_hex, vote_touches_path, }; pub use rpc::handle_rpc_batch; diff --git a/server/src/api/rpc.rs b/server/src/api/rpc.rs index 079d96eb6f1d717c203b1d4aec09f3916384b919..470e54413b9e98825a084a5095c576784c19e169 100644 --- a/server/src/api/rpc.rs +++ b/server/src/api/rpc.rs @@ -27,7 +27,7 @@ use crate::{ use super::auth::{parse_bearer, verify_bearer_principal}; use super::helpers::{ compute_connectivity_stats, is_pair_voted, now_ms, paginate_rankings, parse_parent_specs, - pick_random_distinct_canonical, resolve_item, vote_touches_path, + pick_random_distinct_item_pair, resolve_item, vote_touches_path, }; use super::validate::validate_ingest_document; @@ -721,7 +721,7 @@ async fn rpc_get_pair(state: &AppState, room: String, parent_path: String) -> Re let content = reduced.content.entry(scope.clone()).or_default(); let group = &mut content.ranking_group; if group.idx_to_item.is_empty() { - pick_random_distinct_canonical(&pool) + pick_random_distinct_item_pair(&pool) } else { let mut rng = rand::thread_rng(); let idxs: Vec = pool @@ -767,7 +767,7 @@ async fn rpc_get_pair(state: &AppState, room: String, parent_path: String) -> Re } } } - pick.or_else(|| pick_random_distinct_canonical(&pool)) + pick.or_else(|| pick_random_distinct_item_pair(&pool)) } }; let Some((left, right)) = selected else { diff --git a/server/src/external_resolver.rs b/server/src/external_resolver.rs index 6f5c982a627f9f620f0e3be0ba0cb92a3d7d4bb7..13f06b922a685f110d269f7397da807cf8600fe1 100644 --- a/server/src/external_resolver.rs +++ b/server/src/external_resolver.rs @@ -11,7 +11,7 @@ pub trait ExternalResolver: Send + Sync { fn normalize(&self, path: &str) -> String; /// Fetches body when missing; GitHub hook lands here in a follow-up. - async fn fetch_body(&self, canonical_url: &ItemId) -> Result; + async fn fetch_body(&self, item: &ItemId) -> Result; } /// Placeholder until domain-specific resolvers exist. @@ -27,7 +27,7 @@ impl ExternalResolver for DefaultExternalResolver { path.to_string() } - async fn fetch_body(&self, _canonical_url: &ItemId) -> Result { + async fn fetch_body(&self, _item: &ItemId) -> Result { Err("external fetch not implemented".to_string()) } } diff --git a/server/src/html/breadcrumb_path.rs b/server/src/html/breadcrumb_path.rs index 5743a98753f579c70e10961278469afd7cb9ddcf..3e949ce2cb70de3f024f46af773a93f3f8852260 100644 --- a/server/src/html/breadcrumb_path.rs +++ b/server/src/html/breadcrumb_path.rs @@ -2,33 +2,33 @@ use crate::path_types::{tilde_http_path_to_item_id, ItemId}; /// Semantic view of an ontology path for rendering and routing decisions. pub(super) struct OntologyPath { - canonical: ItemId, + item: ItemId, /// Breadcrumb segments: for `~/a/b` this is `["a", "b"]` (leading `~` rendered separately). segments: Vec, } impl OntologyPath { /// Path is the `*path` segment from `/~/*path` (e.g. `topic/a`). Always treat it as under `~/` - /// so it canonicalizes to `https://slug.social/~/…`, not the non-tilde site path. + /// so it resolves to `https://slug.social/~/…`, not the non-tilde site path. pub(super) fn from_input(path: &str) -> Self { - let canonical = tilde_http_path_to_item_id(path); - Self::from_canonical(canonical) + let item = tilde_http_path_to_item_id(path); + Self::from_item(item) } - pub(super) fn from_canonical(canonical: ItemId) -> Self { + pub(super) fn from_item(item: ItemId) -> Self { // tilde_segments() returns ["~", "a", "b"] but bc_path() renders "~" itself, // so we skip the leading "~" segment here. - let segments = canonical + let segments = item .tilde_segments() .into_iter() .skip(1) // drop the leading "~" .map(|s| s.to_string()) .collect(); - Self { canonical, segments } + Self { item, segments } } pub(super) fn root() -> Self { - Self::from_canonical(ItemId::ontology_root()) + Self::from_item(ItemId::ontology_root()) } pub(super) fn is_root(&self) -> bool { @@ -50,13 +50,13 @@ impl OntologyPath { } pub(super) fn as_str(&self) -> &str { - self.canonical.as_str() + self.item.as_str() } } /// External `https://host/…` items addressed as `/-/host/…` in the URL bar. pub(super) struct ExternalOntologyPath { - canonical: ItemId, + item: ItemId, /// e.g. `["github.com", "org", "repo", "issues"]` segments: Vec, } @@ -71,14 +71,14 @@ impl ExternalOntologyPath { } else { format!("-/{}", p.trim_start_matches('/')) }; - let Some(canonical) = ItemId::parse(&raw) else { - return Self::from_canonical(ItemId::opaque("https://.".to_string())); + let Some(parsed) = ItemId::parse(&raw) else { + return Self::from_item(ItemId::opaque("https://.".to_string())); }; - Self::from_canonical(canonical) + Self::from_item(parsed) } - pub(super) fn from_canonical(canonical: ItemId) -> Self { - let s = canonical.as_str(); + pub(super) fn from_item(item: ItemId) -> Self { + let s = item.as_str(); let rest = s .strip_prefix("https://") .or_else(|| s.strip_prefix("http://")) @@ -93,7 +93,7 @@ impl ExternalOntologyPath { } else { segments }; - Self { canonical, segments } + Self { item, segments } } pub(super) fn is_root(&self) -> bool { @@ -105,6 +105,6 @@ impl ExternalOntologyPath { } pub(super) fn as_str(&self) -> &str { - self.canonical.as_str() + self.item.as_str() } } diff --git a/server/src/html/garden.rs b/server/src/html/garden.rs index 2af9ac5bfdee1630883e6f8257883baeafcb5a44..01b665b207f7ad4a2dac60153b9165679aaa9ab7 100644 --- a/server/src/html/garden.rs +++ b/server/src/html/garden.rs @@ -32,7 +32,7 @@ fn item_display_path(item: &str) -> String { .unwrap_or_else(|| canonicalize_item(item)) } -/// Canonical ontology URL for an item path. +/// Garden href for an item path string in this nav scope. fn item_href(item: &str, nav: &ThreadNav) -> String { nav.garden_item_url(item) } @@ -67,7 +67,7 @@ enum GardenBrowsePath { } impl GardenBrowsePath { - fn canonical_url(&self) -> &str { + fn item(&self) -> &str { match self { GardenBrowsePath::Tilde(p) => p.as_str(), GardenBrowsePath::External(p) => p.as_str(), @@ -669,7 +669,7 @@ async fn render_scope_view( let scope = nav.scope(); let model = { let reduced = state.reduced.read().await; - build_item_page_view_model(&reduced, &scope, browse.canonical_url()) + build_item_page_view_model(&reduced, &scope, browse.item()) }; let thread_href = |tag: &str| nav.thread_url(tag); let external_empty_body = browse.is_external() && model.body.is_none(); diff --git a/server/src/html/mod.rs b/server/src/html/mod.rs index 668403a35c415e6f091362b28c63ac294057fb6d..d6571df9b98726c0d3a82516e13404b74966281e 100644 --- a/server/src/html/mod.rs +++ b/server/src/html/mod.rs @@ -376,7 +376,7 @@ fn bc_path_external(path: &ExternalOntologyPath) -> Markup { } } -/// Breadcrumb for any canonical path, e.g. "parables" or "parables/counting-the-cost". +/// Breadcrumb for any ontology path segment, e.g. "parables" or "parables/counting-the-cost". fn bc_path(path: &OntologyPath) -> Markup { html! { a href=(path.slug_root_href()) { "slug.social" } diff --git a/server/src/reducer.rs b/server/src/reducer.rs index cd11a5391de8e36b1c3bb6c9b8067ceba0878e31..8751595b1dbe472079021994bd452a0234d59af5 100644 --- a/server/src/reducer.rs +++ b/server/src/reducer.rs @@ -88,7 +88,7 @@ impl GroupState { if let Some(canon) = ItemId::parse(item) { self.ensure_item(&canon) } else { - // Fallback: treat as raw canonical string + // Fallback: treat as raw storage string let canon = ItemId::opaque(item.to_string()); self.ensure_item(&canon) } @@ -212,7 +212,7 @@ pub struct ContentState { pub ranking_group: GroupState, pub items: HashSet, pub item_bodies: HashMap, - /// Parent canonical URL -> direct children. + /// Parent [`ItemId`] -> direct children. pub item_children: HashMap>, /// Per-item vote history (most recent first). pub item_votes: HashMap>, @@ -365,7 +365,7 @@ impl ReducerState { } } - /// Resolve an item path as a first-class canonical path. + /// Resolve an item path as a first-class [`ItemId`]. fn normalize_item(item: &str) -> Option { ItemId::parse(item) } diff --git a/types/src/item_id.rs b/types/src/item_id.rs index 5fe9fad39c0d58e546c0a0162eba4348d64e0865..3411c35cd642d4bc4fc27fcb96a238d32dbd07a5 100644 --- a/types/src/item_id.rs +++ b/types/src/item_id.rs @@ -12,7 +12,7 @@ use crate::item_wire::{ /// Structural key for items in [`slug_types`] and the server reducer. /// -/// Wire / JSON uses the same single string as the former canonical item URL (via serde). +/// Wire / JSON uses the same single normalized storage string (via serde). #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum ItemId { /// Tilde ontology root (`~/`); storage [`SLUG_TILDE_ONTOLOGY_ROOT`]. @@ -65,6 +65,8 @@ impl ItemId { self.as_str().to_string() } + /// Tail after `https://slug.social/~/` for ontology items (empty string at root), or `None` + /// when this id is not under the tilde ontology. pub fn tilde_tail(&self) -> Option<&str> { match self { ItemId::Root => Some(""), @@ -81,11 +83,6 @@ impl ItemId { } } - /// HTTP garden tail after `~/` (empty at ontology root), or `None` if not under tilde ontology. - pub fn tilde_http_tail(&self) -> Option { - self.tilde_tail().map(str::to_owned) - } - pub fn last_segment(&self) -> &str { let s = self.as_str(); s.rsplit('/').find(|x| !x.is_empty()).unwrap_or(s) diff --git a/types/src/paths.rs b/types/src/paths.rs index a1d066d83e7fbcefc27707dd682e54dbcec1cc56..8669f6872b9fa71d72a3f5c346945427bc00ac5a 100644 --- a/types/src/paths.rs +++ b/types/src/paths.rs @@ -334,19 +334,19 @@ mod tests { use super::*; #[test] - fn canonical_parent_deep() { + fn item_parent_deep() { let c = ItemId::parse("~/a/b/c").unwrap(); assert_eq!(c.parent().unwrap().as_str(), "https://slug.social/~/a/b"); } #[test] - fn canonical_parent_one_level() { + fn item_parent_one_level() { let c = ItemId::parse("~/a").unwrap(); assert_eq!(c.parent().unwrap().as_str(), "https://slug.social/~"); } #[test] - fn canonical_parent_root_is_none() { + fn item_parent_root_is_none() { let root = ItemId::parse("~/").unwrap(); assert!(root.parent().is_none()); assert_eq!(root.as_str(), SLUG_TILDE_ONTOLOGY_ROOT);