pub fn json_str(value: &str) -> String {
let mut out = String::from("\"");
for c in value.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
out
}
pub fn field(line: &str, name: &str) -> Option<String> {
let key = format!("\"{name}\":\"");
let start = line.find(&key)? + key.len();
let mut out = String::new();
let mut chars = line[start..].chars();
while let Some(c) = chars.next() {
match c {
'\\' => match chars.next()? {
'n' => out.push('\n'),
't' => out.push('\t'),
'r' => out.push('\r'),
other => out.push(other),
},
'"' => return Some(out),
c => out.push(c),
}
}
None
}
pub fn now_rfc3339() -> String {
chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
}
pub fn read_lines(path: &std::path::Path) -> Vec<String> {
let Ok(text) = std::fs::read_to_string(path) else {
return Vec::new();
};
text.lines()
.filter(|l| !l.trim().is_empty())
.rev()
.map(str::to_string)
.collect()
}
pub fn append(path: &std::path::Path, record: &str, what: &str) -> ikigai_core::Result<()> {
use ikigai_core::Error;
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir).map_err(|e| Error::Endpoint(format!("{what} dir: {e}")))?;
}
use std::io::Write;
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.map_err(|e| Error::Endpoint(format!("open {}: {e}", path.display())))?;
file.write_all(record.as_bytes())
.map_err(|e| Error::Endpoint(format!("append: {e}")))?;
Ok(())
}