use crate::cli::ClaimKindArg;
use crate::contracts::args::parse_kind;
use crate::slash::SlashParseError;
#[derive(Debug)]
pub(crate) struct RememberSpec {
pub table: String,
pub kind: ClaimKindArg,
pub value: String,
pub column: Option<String>,
pub reason: Option<String>,
}
fn is_column_kind(kind: ClaimKindArg) -> bool {
matches!(
kind,
ClaimKindArg::ColumnDescription | ClaimKindArg::ColumnRole
)
}
pub(crate) fn parse_remember(arg: &str) -> Result<RememberSpec, SlashParseError> {
let mut parts = arg.split_whitespace();
let table = parts
.next()
.ok_or_else(|| SlashParseError(usage_remember()))?;
let kind_word = parts
.next()
.ok_or_else(|| SlashParseError(usage_remember()))?;
let kind = parse_kind(kind_word).ok_or_else(|| SlashParseError(usage_remember()))?;
let (column, value) = if is_column_kind(kind) {
let column = parts
.next()
.ok_or_else(|| SlashParseError(usage_remember()))?;
(Some(column.to_string()), rest_after(parts))
} else {
(None, rest_after(parts))
};
let raw_value = value.ok_or_else(|| SlashParseError(usage_remember()))?;
let (value, reason) = split_reason(&raw_value, kind);
Ok(RememberSpec {
table: table.to_string(),
kind,
value,
column,
reason,
})
}
fn rest_after<'a, I: Iterator<Item = &'a str>>(mut parts: I) -> Option<String> {
let first = parts.next()?;
let mut value = first.to_string();
for token in parts {
value.push(' ');
value.push_str(token);
}
Some(value)
}
fn split_reason(value: &str, kind: ClaimKindArg) -> (String, Option<String>) {
if !is_directive_kind(kind) {
return (value.to_string(), None);
}
let lower = value.to_ascii_lowercase();
let Some(idx) = find_standalone(&lower, "because") else {
return (value.to_string(), None);
};
let reason = value[idx + "because".len()..].trim();
let value_part = value[..idx].trim_end();
if reason.is_empty() || value_part.is_empty() {
return (value.to_string(), None);
}
(value_part.to_string(), Some(reason.to_string()))
}
fn is_directive_kind(kind: ClaimKindArg) -> bool {
matches!(
kind,
ClaimKindArg::Grain | ClaimKindArg::TimeColumn | ClaimKindArg::ColumnRole
)
}
fn find_standalone(haystack: &str, needle: &str) -> Option<usize> {
let mut start = 0;
while let Some(idx) = haystack[start..].find(needle) {
let abs = start + idx;
let before_ok = abs == 0 || haystack.as_bytes().get(abs - 1) == Some(&b' ');
let after = abs + needle.len();
let after_ok = after >= haystack.len() || haystack.as_bytes().get(after) == Some(&b' ');
if before_ok && after_ok {
return Some(abs);
}
start = abs + needle.len();
}
None
}
fn usage_remember() -> String {
"/remember <catalog.schema.object> <kind> <value…> [because <reason…>]\n\
kinds: description, alias, grain, time-column, column-description <column> <value…>, \
column-role <column> <role>\n\
`because <reason…>` is optional, and only the directive kinds (grain, time-column, \
column-role) carry it"
.into()
}
#[cfg(test)]
#[path = "remember_tests.rs"]
mod tests;