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}