use std::collections::BTreeMap;
use onemessagebus::sdk_schema::RegistryDocument;
use onemessagebus::{
BundleVersion, CheckStep, FieldPath, GrantOps, GrantStep, Grants, LayoutAuthor, LayoutDocument,
LayoutName, MemberName, NonEmpty, OpWord, Predicate, PrepareStep, RefusalReason, RenameStep,
Route, RouteStep, SchemaBundle, SchemaId, StampStep, VersionStep,
};
use serde_json::Value;
use super::{
allowlist, name, queues, registry, source, Op, COMMANDS, COMMAND_OUTCOME_SCHEMA,
PLANNER_CHANNEL, QUEUED_COMMANDS_SCHEMA, QUEUED_REPLY_SCHEMA, READ_REPLY_ENVELOPE_SCHEMA,
REPLIES, REPLY_ENVELOPE_VERSION, REPLY_ENVELOPE_VERSIONS_READ, SURFACES, SURFACE_SCHEMA,
};
pub const DOCUMENT_PATH: &str = "schemas/planner-channel.json";
pub const DOCUMENT_VERSION: &str = "1.0.0";
const REPLY_ENVELOPE_SCHEMA: SchemaId = SchemaId::literal("agent", "reply-envelope", 3);
#[derive(Clone, Copy)]
enum Envelope {
Bare,
Framed,
}
impl Envelope {
fn at(self, member: &str) -> FieldPath {
match self {
Self::Bare => field(member),
Self::Framed => field(&format!("reply.{member}")),
}
}
fn when(self) -> Predicate {
let framed = Predicate::Present {
field: field("reply"),
present: true,
};
match self {
Self::Bare => Predicate::Not(Box::new(framed)),
Self::Framed => framed,
}
}
}
fn field(text: &str) -> FieldPath {
text.parse()
.unwrap_or_else(|_| unreachable!("{text} is a field path"))
}
fn member(text: &str) -> MemberName {
MemberName::new(text).unwrap_or_else(|why| unreachable!("{text} is a member name: {why}"))
}
fn members(texts: &[&str]) -> NonEmpty<MemberName> {
NonEmpty::new(texts.iter().map(|text| member(text)).collect())
.unwrap_or_else(|why| unreachable!("{texts:?} names a member: {why}"))
}
fn refusal(text: &str) -> RefusalReason {
serde_json::from_value(Value::from(text))
.unwrap_or_else(|why| unreachable!("{text:?} is a refusal: {why}"))
}
fn grant(author: FieldPath, ops: GrantOps, when: Option<Predicate>) -> GrantStep {
GrantStep {
author,
default_author: Some(onemessagebus::Author::from("planner")),
ops,
refusal: Some(refusal(
"'{op}' is not an op the {author} may issue: {reason}. Surface it to the planner \
instead",
)),
unknown: Some(refusal(
"'{op}' is not an op of the planner channel; the ops are: {ops}",
)),
undeclared: Some(refusal(
"the envelope's author `{author}` is not declared; the declared authors are: \
{authors}",
)),
malformed: Some(refusal("the envelope's author: {why}")),
when,
}
}
fn envelope_checks(at: Envelope) -> Vec<PrepareStep> {
let when = at.when();
let also = |predicate: Predicate| Some(Predicate::All(vec![when.clone(), predicate]));
let older: Vec<u64> = REPLY_ENVELOPE_VERSIONS_READ
.iter()
.filter(|version| **version != REPLY_ENVELOPE_VERSION)
.map(|version| u64::from(*version))
.collect();
let edits = Predicate::NonEmpty {
field: at.at("commands"),
non_empty: true,
};
let (reads, required_when) = match at {
Envelope::Bare => (older, edits),
Envelope::Framed => {
let read_as_current = older
.into_iter()
.map(|version| Predicate::equals(at.at("version"), version))
.collect();
(
Vec::new(),
Predicate::All(vec![
edits,
Predicate::Not(Box::new(Predicate::Any(read_as_current))),
]),
)
}
};
vec![
PrepareStep::Check(CheckStep {
schema: READ_REPLY_ENVELOPE_SCHEMA,
at: match at {
Envelope::Bare => None,
Envelope::Framed => Some(field("reply")),
},
refusal: refusal("the reply is malformed: {why}"),
when: Some(when.clone()),
}),
PrepareStep::Grant(grant(
at.at("author"),
GrantOps::AuthorOnly,
Some(when.clone()),
)),
PrepareStep::Grant(GrantStep {
refusal: Some(refusal(
"declaring the run complete is not something the {author} may do: {reason}. \
Surface it to the planner instead",
)),
..grant(
at.at("author"),
GrantOps::Word(OpWord(Op::Complete.word().to_owned())),
also(Predicate::equals(at.at("completion"), true)),
)
}),
PrepareStep::Version(VersionStep {
at: at.at("version"),
value: u64::from(REPLY_ENVELOPE_VERSION),
reads,
required_when: Some(required_when),
refusal: refusal("an edit envelope requires version {value}"),
when: Some(when.clone()),
}),
PrepareStep::Grant(grant(
at.at("author"),
GrantOps::Each {
each: at.at("commands"),
op: field("op"),
},
Some(when),
)),
]
}
#[must_use]
pub fn prepare() -> BTreeMap<onemessagebus::QueueName, Vec<PrepareStep>> {
let verdict = ["completion", "message", "reason"];
let mut replies = envelope_checks(Envelope::Framed);
replies.extend(envelope_checks(Envelope::Bare));
replies.push(PrepareStep::Route(RouteStep {
routes: NonEmpty::new(vec![
Route {
queue: name(COMMANDS),
on: members(&["commands"]),
take: members(&["author", "commands"]),
under: None,
stamp: Vec::new(),
},
Route {
queue: name(REPLIES),
on: members(&verdict),
take: members(&[
"version",
"author",
"completion",
"message",
"reason",
"commands",
]),
under: Some(member("reply")),
stamp: vec![member("at")],
},
])
.unwrap_or_else(|why| unreachable!("two routes: {why}")),
fallback: Some(name(REPLIES)),
when: Some(Envelope::Bare.when()),
}));
BTreeMap::from([
(
name(SURFACES),
vec![
PrepareStep::Rename(RenameStep {
from: member(onemessagebus::ask::ABOUT),
to: member("workstream"),
when: None,
}),
PrepareStep::Stamp(StampStep {
member: member("queued_at"),
when: None,
}),
],
),
(name(REPLIES), replies),
(
name(COMMANDS),
vec![PrepareStep::Grant(grant(
field("author"),
GrantOps::Each {
each: field("commands"),
op: field("op"),
},
None,
))],
),
])
}
#[must_use]
pub fn document() -> LayoutDocument {
let allowlist = allowlist();
let vocabulary: Vec<OpWord> = Op::ALL
.iter()
.map(|op| OpWord(op.word().to_owned()))
.collect();
let authors = allowlist
.authors()
.into_iter()
.map(|author| {
let granted = allowlist.granted(&author);
let grants = if granted.len() == Op::ALL.len() {
Grants::EveryOp
} else {
Grants::Only(
granted
.iter()
.map(|op| OpWord(op.word().to_owned()))
.collect(),
)
};
let refusals = Op::ALL
.iter()
.filter(|op| !granted.contains(op))
.filter_map(|op| {
let reason = allowlist.allows(&author, op).err()?.reason;
Some((OpWord(op.word().to_owned()), refusal(&reason)))
})
.collect();
(author, LayoutAuthor { grants, refusals })
})
.collect();
LayoutDocument::new(
LayoutName::new(PLANNER_CHANNEL).unwrap_or_else(|why| unreachable!("{why}")),
Some(format!(
"onepipeline's planner channel: the surfaces a planner is asked about \
(`{SURFACES}`, answered on `{REPLIES}`), the replies and command envelopes it \
writes, and what each envelope was answered with. Generated from the layout \
this engine compiles in; a surface of source `{}` supersedes a waiting one.",
source::CHECK_IN
)),
NonEmpty::new(queues()).unwrap_or_else(|why| unreachable!("four queues: {why}")),
vocabulary,
authors,
prepare(),
)
.unwrap_or_else(|why| panic!("the planner-channel layout is not a layout document: {why}"))
}
fn schemas() -> Vec<RegistryDocument> {
let registry = registry();
let mut ids = vec![
SURFACE_SCHEMA,
QUEUED_REPLY_SCHEMA,
QUEUED_COMMANDS_SCHEMA,
COMMAND_OUTCOME_SCHEMA,
];
ids.push(READ_REPLY_ENVELOPE_SCHEMA);
ids.extend(REPLY_ENVELOPE_VERSIONS_READ.iter().rev().map(|version| {
REPLY_ENVELOPE_SCHEMA.at((*version)
.try_into()
.unwrap_or_else(|_| unreachable!("a reply envelope version is above zero")))
}));
ids.into_iter()
.map(|id| RegistryDocument {
schema: registry
.schema(&id)
.cloned()
.unwrap_or_else(|| unreachable!("the layout registers {id}")),
id,
})
.collect()
}
#[must_use]
pub fn bundle() -> SchemaBundle {
SchemaBundle::with_layouts(
DOCUMENT_VERSION
.parse::<BundleVersion>()
.unwrap_or_else(|why| unreachable!("{why}")),
Some(
"The planner-channel layout onepipeline publishes for a host's onemessagebus to link."
.to_owned(),
),
schemas(),
vec![document()],
)
.unwrap_or_else(|why| panic!("the planner-channel bundle is not a schema bundle: {why}"))
}
#[must_use]
pub fn bundle_json() -> String {
let mut text = serde_json::to_string_pretty(&bundle()).expect("a bundle serializes");
text.push('\n');
text
}