Skip to main content

archelon_core/
entry_ref.rs

1use std::path::PathBuf;
2
3use caretta_id::CarettaId;
4use schemars::JsonSchema;
5use serde::Deserialize;
6
7/// A reference to a journal entry — a filesystem path, a CarettaId, or a title.
8///
9/// This is the canonical input type for commands that operate on a single entry
10/// (show, fix, remove, etc.).  Parse raw CLI user input with [`EntryRef::parse`],
11/// then resolve it to a concrete [`PathBuf`] via [`ops::resolve_entry`].
12///
13/// # Syntax (CLI)
14///
15/// | Input form              | Resolved as     |
16/// |-------------------------|-----------------|
17/// | `@abc1234`              | `Id(CarettaId)` |
18/// | `path/to/file.md`       | `Path(...)`     |
19/// | `./relative.md`         | `Path(...)`     |
20/// | `~/absolute.md`         | `Path(...)`     |
21/// | `anything_else`         | `Title(...)`    |
22///
23/// The `@` prefix is required for IDs to avoid ambiguity with titles that
24/// happen to be 7 alphanumeric characters.  If the part after `@` cannot be
25/// parsed as a valid [`CarettaId`], the `@` is treated as part of the string
26/// and the usual path/title heuristics apply.
27#[derive(Debug, Clone, Deserialize, JsonSchema)]
28#[serde(rename_all = "snake_case")]
29pub enum EntryRef {
30    /// A filesystem path to the entry file.
31    Path(PathBuf),
32    /// A fully-parsed CarettaId (the `@` prefix has been stripped and validated).
33    Id(CarettaId),
34    /// An exact entry title (case-sensitive).
35    Title(String),
36}
37
38impl EntryRef {
39    /// Classify a raw CLI string as a path, an ID, or a title.
40    ///
41    /// - Starts with `@` **and** the remainder parses as a [`CarettaId`]
42    ///   → [`EntryRef::Id`].
43    /// - Contains `/` or `\`, starts with `.` or `~`, or ends with `.md`
44    ///   → [`EntryRef::Path`].
45    /// - Anything else (including `@foo` where `foo` is not a valid CarettaId)
46    ///   → [`EntryRef::Title`].
47    pub fn parse(s: &str) -> Self {
48        if let Some(rest) = s.strip_prefix('@') {
49            if let Ok(id) = rest.parse::<CarettaId>() {
50                return EntryRef::Id(id);
51            }
52            // Invalid CarettaId after `@` — fall through to path/title heuristics.
53        }
54        if s.contains('/')
55            || s.contains(std::path::MAIN_SEPARATOR)
56            || s.starts_with('.')
57            || s.starts_with('~')
58            || s.ends_with(".md")
59        {
60            EntryRef::Path(PathBuf::from(s))
61        } else {
62            EntryRef::Title(s.to_owned())
63        }
64    }
65}
66
67impl From<&str> for EntryRef {
68    fn from(s: &str) -> Self {
69        Self::parse(s)
70    }
71}
72
73impl From<String> for EntryRef {
74    fn from(s: String) -> Self {
75        Self::parse(&s)
76    }
77}