254 lines
8.8 KiB
Rust
254 lines
8.8 KiB
Rust
//! Review (off by default, `[memory] review = true`): identity writes wait in
|
|
//! `memory/pending.jsonl` for the owner's `accept` or `reject` instead of being applied (M5a spec,
|
|
//! section 5). Notes are never staged.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use bxcore::escape::escape_model_text;
|
|
use bxcore::{DataClass, MemoryArgs, Timestamp};
|
|
|
|
use crate::edit::{Context, apply};
|
|
use crate::files::Store;
|
|
use crate::journal::{self, Record};
|
|
|
|
/// One line of `pending.jsonl`: a write waiting, or what became of one.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
|
pub enum Line {
|
|
Waiting {
|
|
id: String,
|
|
time: String,
|
|
session: String,
|
|
call: u64,
|
|
taint: DataClass,
|
|
/// The call's arguments, as `brokerd` sent them.
|
|
arguments: String,
|
|
},
|
|
Accepted {
|
|
id: String,
|
|
time: String,
|
|
edit: String,
|
|
},
|
|
Rejected {
|
|
id: String,
|
|
time: String,
|
|
},
|
|
}
|
|
|
|
fn lines(store: &Store) -> Result<Vec<Line>, String> {
|
|
// Read `store.path("pending.jsonl")`: NotFound -> Ok(empty); another error -> Err(format!("{}:
|
|
// {e}", path.display())). Parse every line as a Line with serde_json; a bad one ->
|
|
// Err(format!("{}: line {}: {e}", path.display(), n + 1)).
|
|
let path = store.path("pending.jsonl");
|
|
let text = match std::fs::read_to_string(&path) {
|
|
Ok(t) => t,
|
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
|
|
Err(e) => return Err(format!("{}: {e}", path.display())),
|
|
};
|
|
let mut out: Vec<Line> = Vec::new();
|
|
for (n, line) in text.lines().enumerate() {
|
|
let entry: Line = serde_json::from_str(line)
|
|
.map_err(|e| format!("{}: line {}: {e}", path.display(), n + 1))?;
|
|
out.push(entry);
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
fn add(store: &Store, line: &Line) -> Result<(), String> {
|
|
// `serde_json::to_string(line)` (its error as a String), then `store.append("pending.jsonl",
|
|
// &format!("{text}\n"))`.
|
|
let text = serde_json::to_string(line).map_err(|e| e.to_string())?;
|
|
store.append("pending.jsonl", &format!("{text}\n"))
|
|
}
|
|
|
|
/// The writes still waiting: `(id, session, arguments, taint, call)`.
|
|
fn waiting(store: &Store) -> Result<Vec<Line>, String> {
|
|
// The ids that have an Accepted or a Rejected line are done. Return a clone of every Waiting
|
|
// line whose id is not done, in file order.
|
|
let all = lines(store)?;
|
|
let done: Vec<String> = all
|
|
.iter()
|
|
.filter_map(|line| match line {
|
|
Line::Accepted { id, .. } | Line::Rejected { id, .. } => Some(id.clone()),
|
|
Line::Waiting { .. } => None,
|
|
})
|
|
.collect();
|
|
Ok(all
|
|
.iter()
|
|
.filter(|line| match line {
|
|
Line::Waiting { id, .. } => !done.iter().any(|d| d == id),
|
|
Line::Accepted { .. } | Line::Rejected { .. } => false,
|
|
})
|
|
.cloned()
|
|
.collect())
|
|
}
|
|
|
|
/// Stage an identity write. Its shape is checked now; its content when it is accepted.
|
|
pub fn submit(
|
|
store: &Store,
|
|
args: &MemoryArgs,
|
|
arguments: &str,
|
|
ctx: &Context,
|
|
) -> Result<String, String> {
|
|
// 1. `args.check()?`.
|
|
// 2. `day` is the first 10 characters of `ctx.now.to_rfc3339()`. `taken` = how many Waiting
|
|
// lines of `lines(store)?`, done or not, have an id starting format!("p-{day}-"): an id is
|
|
// never used twice. The id is format!("p-{day}-{:04}", taken + 1).
|
|
// 3. `add` a Waiting line: id, time `ctx.now.to_rfc3339()`, session, call and taint from ctx,
|
|
// arguments as given.
|
|
// 4. Ok(format!("waiting for the owner's review ({id}); nothing is changed until it is
|
|
// accepted")).
|
|
args.check()?;
|
|
let rfc = ctx.now.to_rfc3339();
|
|
let day = rfc.get(..10).unwrap_or("0000-00-00");
|
|
let taken = lines(store)?
|
|
.iter()
|
|
.filter(|line| match line {
|
|
Line::Waiting { id, .. } => id.starts_with(&format!("p-{day}-")),
|
|
_ => false,
|
|
})
|
|
.count();
|
|
let id = format!("p-{day}-{:04}", taken + 1);
|
|
add(
|
|
store,
|
|
&Line::Waiting {
|
|
id: id.clone(),
|
|
time: rfc,
|
|
session: ctx.session.clone(),
|
|
call: ctx.call,
|
|
taint: ctx.taint,
|
|
arguments: arguments.to_string(),
|
|
},
|
|
)?;
|
|
Ok(format!(
|
|
"waiting for the owner's review ({id}); nothing is changed until it is accepted"
|
|
))
|
|
}
|
|
|
|
/// The writes waiting, one per line, arguments escaped.
|
|
pub fn list(store: &Store) -> Result<String, String> {
|
|
// One line per `waiting` entry: format!("{id} {session} {}", escape_model_text(arguments)),
|
|
// joined by newlines; with none, "nothing is waiting for review".
|
|
let waits = waiting(store)?;
|
|
let lines: Vec<String> = waits
|
|
.iter()
|
|
.filter_map(|l| match l {
|
|
Line::Waiting {
|
|
id,
|
|
session,
|
|
arguments,
|
|
..
|
|
} => Some(format!("{id} {session} {}", escape_model_text(arguments))),
|
|
Line::Accepted { .. } | Line::Rejected { .. } => None,
|
|
})
|
|
.collect();
|
|
if lines.is_empty() {
|
|
Ok("nothing is waiting for review".to_string())
|
|
} else {
|
|
Ok(lines.join("\n"))
|
|
}
|
|
}
|
|
|
|
/// Apply a waiting write, with the same checks, against the file as it is now, and journal it.
|
|
pub fn accept(store: &Store, id: &str, now: Timestamp) -> Result<String, String> {
|
|
// 1. The Waiting line with this id among `waiting(store)?`, else Err(format!("nothing waiting
|
|
// as {id}")).
|
|
// 2. Parse its arguments as MemoryArgs (the error as a String).
|
|
// 3. A Context from its session, call and taint, untrusted false, now.
|
|
// 4. `apply(store, &args, &ctx)?`, then `journal::record(store, &ctx, &outcome)?` gives the
|
|
// edit id.
|
|
// 5. `add` an Accepted line { id, time now, edit }.
|
|
// 6. Ok(format!("accepted: {id} as {edit}: {}", outcome.answer)).
|
|
let waiting_lines = waiting(store)?;
|
|
let found = waiting_lines
|
|
.iter()
|
|
.find(|l| match l {
|
|
Line::Waiting { id: l_id, .. } => l_id == id,
|
|
Line::Accepted { .. } | Line::Rejected { .. } => false,
|
|
})
|
|
.ok_or_else(|| format!("nothing waiting as {id}"))?;
|
|
let (args, session, call, taint) = match found {
|
|
Line::Waiting {
|
|
arguments,
|
|
session,
|
|
call,
|
|
taint,
|
|
..
|
|
} => {
|
|
let args: MemoryArgs = serde_json::from_str(arguments).map_err(|e| e.to_string())?;
|
|
(args, session.clone(), *call, *taint)
|
|
}
|
|
Line::Accepted { .. } | Line::Rejected { .. } => {
|
|
return Err(format!("nothing waiting as {id}"));
|
|
}
|
|
};
|
|
let ctx = Context {
|
|
session,
|
|
call,
|
|
taint,
|
|
untrusted: false,
|
|
now,
|
|
};
|
|
let outcome = apply(store, &args, &ctx)?;
|
|
let edit = journal::record(store, &ctx, &outcome)?;
|
|
add(
|
|
store,
|
|
&Line::Accepted {
|
|
id: id.to_string(),
|
|
time: now.to_rfc3339(),
|
|
edit: edit.clone(),
|
|
},
|
|
)?;
|
|
Ok(format!("accepted: {id} as {edit}: {}", outcome.answer))
|
|
}
|
|
|
|
/// Discard a waiting write; the journal records it.
|
|
pub fn reject(store: &Store, id: &str, now: Timestamp) -> Result<String, String> {
|
|
// 1. The Waiting line with this id, else Err(format!("nothing waiting as {id}")).
|
|
// 2. `add` a Rejected line { id, time now }.
|
|
// 3. `journal::append` a Record: its session and call, action "reject", file None, to empty, no
|
|
// entries, no notes, untrusted false, flag None, undoes Some(id).
|
|
// 4. Ok(format!("rejected: {id}")).
|
|
let waiting_lines = waiting(store)?;
|
|
let found = waiting_lines
|
|
.iter()
|
|
.find(|l| match l {
|
|
Line::Waiting { id: l_id, .. } => l_id == id,
|
|
Line::Accepted { .. } | Line::Rejected { .. } => false,
|
|
})
|
|
.ok_or_else(|| format!("nothing waiting as {id}"))?;
|
|
let (session, call) = match found {
|
|
Line::Waiting { session, call, .. } => (session.clone(), *call),
|
|
Line::Accepted { .. } | Line::Rejected { .. } => {
|
|
return Err(format!("nothing waiting as {id}"));
|
|
}
|
|
};
|
|
add(
|
|
store,
|
|
&Line::Rejected {
|
|
id: id.to_string(),
|
|
time: now.to_rfc3339(),
|
|
},
|
|
)?;
|
|
journal::append(
|
|
store,
|
|
Record {
|
|
id: String::new(),
|
|
time: now.to_rfc3339(),
|
|
session,
|
|
call,
|
|
action: "reject".to_string(),
|
|
file: None,
|
|
to: String::new(),
|
|
before: Vec::new(),
|
|
after: Vec::new(),
|
|
notes: Vec::new(),
|
|
untrusted: false,
|
|
flag: None,
|
|
undoes: Some(id.to_string()),
|
|
},
|
|
now,
|
|
)?;
|
|
Ok(format!("rejected: {id}"))
|
|
}
|