Skip to main content

omgbase_surface/
paths.rs

1//! The two path forms (`spec/surface` §1 "Paths", 2.0). The store keeps a
2//! document's path in git's repo-relative form (`projects/oqx.md`: `docs.path`,
3//! the adapters, every store/sync spec), while every reference an author
4//! writes — a Markdown link, a wikilink, a frontmatter relation — is
5//! root-absolute (`/projects/oqx.md`). The surface speaks the reference form:
6//! every path a query, a tool or a CLI verb RETURNS is `/`-rooted, and every
7//! path they ACCEPT tolerates both forms. These two functions are the whole
8//! conversion; the store never sees a rooted path and the surface never hands
9//! out a bare one. Port of `packages/core/src/core/paths.ts`.
10
11use serde_json::Value as Json;
12
13/// The reference (surface) form of a storage path: exactly one leading `/`.
14/// `""` (the repo root, `docs_tree`'s prefix) becomes `/`.
15#[must_use]
16pub fn reference_path(path: &str) -> String {
17    format!("/{}", storage_path(path))
18}
19
20/// The storage form of a path a caller handed in: every leading `/` stripped
21/// (a missing slash is fine, an extra one is forgiven).
22#[must_use]
23pub fn storage_path(path: &str) -> &str {
24    path.trim_start_matches('/')
25}
26
27/// [`reference_path`] over a JSON string value; anything else passes through.
28#[must_use]
29pub fn reference_json(v: &Json) -> Json {
30    match v.as_str() {
31        Some(s) => Json::String(reference_path(s)),
32        None => v.clone(),
33    }
34}
35
36/// Re-key a `{ <path>: … }` object (the dry-run `diffs`) by the reference form,
37/// positions kept.
38#[must_use]
39pub fn reference_keyed(v: &Json) -> Json {
40    match v {
41        Json::Object(m) => Json::Object(
42            m.iter()
43                .map(|(k, x)| (reference_path(k), x.clone()))
44                .collect(),
45        ),
46        other => other.clone(),
47    }
48}
49
50/// Root the string under `key` of a JSON object in place (a non-string or
51/// absent value is left alone).
52pub fn root_field(v: &mut Json, key: &str) {
53    if let Some(m) = v.as_object_mut()
54        && let Some(Json::String(s)) = m.get(key)
55    {
56        let rooted = reference_path(s);
57        m.insert(key.to_owned(), Json::String(rooted));
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use serde_json::json;
65
66    #[test]
67    fn the_two_forms() {
68        assert_eq!(reference_path("a/b.md"), "/a/b.md");
69        assert_eq!(reference_path("/a/b.md"), "/a/b.md");
70        assert_eq!(reference_path("//a.md"), "/a.md");
71        assert_eq!(reference_path(""), "/");
72        assert_eq!(storage_path("/a.md"), "a.md");
73        assert_eq!(storage_path("a.md"), "a.md");
74        assert_eq!(storage_path("///a.md"), "a.md");
75        assert_eq!(storage_path(""), "");
76        assert_eq!(reference_json(&json!("x.md")), json!("/x.md"));
77        assert_eq!(reference_json(&json!(null)), json!(null));
78        let keyed = reference_keyed(&json!({ "a.md": 1, "/b.md": 2 }));
79        assert_eq!(
80            keyed.as_object().unwrap().keys().collect::<Vec<_>>(),
81            ["/a.md", "/b.md"]
82        );
83        let mut v = json!({ "path": "a.md", "n": 1 });
84        root_field(&mut v, "path");
85        root_field(&mut v, "n");
86        root_field(&mut v, "nope");
87        assert_eq!(v, json!({ "path": "/a.md", "n": 1 }));
88    }
89}