use std::collections::BTreeMap;
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::ask::Correlation;
use crate::author::{Allowlist, Author, NarrowingRefused, OpWord};
use crate::codec::{CodecConfig, CodecName};
use crate::kinds::{TransportConfig, TransportKinds};
use crate::layout::LinkedLayout;
use crate::link::{Freshness, LinkError, LinkResolver, Resolved, SchemaLink};
use crate::queue::{
shape_word, Delivery, Ordering, Policy, Predicate, Pushed, QueueError, QueueSpec, RawQueue,
Retention, Supersede,
};
use crate::schema::{Message, Registry, SchemaId};
use crate::transport::{ConsumerName, DocumentName, QueueName, Transport, TransportError};
use crate::validate::{
combined, CommandValidator, OnRecords, PassCache, ValidationContext, Validator, Validators,
Verdict, When,
};
pub const CONFIG_VERSION: u32 = 1;
pub const NARROWED: &str = "the configuration does not grant it";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub version: u32,
pub transport: TransportConfig,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub queues: BTreeMap<QueueName, QueueConfig>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
#[schemars(with = "BTreeMap<ConfiguredAuthor, AuthorConfig>")]
pub authors: BTreeMap<Author, AuthorConfig>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub validators: Vec<ValidatorConfig>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub codecs: BTreeMap<CodecName, CodecConfig>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub schemas: Vec<SchemaLink>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ValidatorConfig {
pub on: QueueName,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub when: Option<When>,
pub kind: ValidatorKind,
pub command: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache: Option<CacheConfig>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum ValidatorKind {
Command,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CacheConfig {
pub dir: PathBuf,
pub bar_fingerprint: Vec<String>,
}
impl ValidatorConfig {
fn build(&self, index: usize) -> Result<CommandValidator, String> {
let ValidatorKind::Command = self.kind;
let validator = CommandValidator::new(self.command.iter().cloned())
.map_err(|failure| format!("validators[{index}].command: {failure}"))?;
Ok(match &self.cache {
Some(cache) => validator.with_cache(
PassCache::new(&cache.dir, cache.bar_fingerprint.iter().cloned()).map_err(
|failure| format!("validators[{index}].cache.bar_fingerprint: {failure}"),
)?,
),
None => validator,
})
}
}
struct Gated {
when: Option<When>,
validator: CommandValidator,
}
impl Validator<Value> for Gated {
fn validate(&self, message: &Value, context: &ValidationContext) -> Verdict {
if self.when.as_ref().is_none_or(|when| when.admits(message)) {
Validator::<Value>::validate(&self.validator, message, context)
} else {
Verdict::Pass
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct QueueConfig {
#[serde(default, skip_serializing_if = "PolicyConfig::is_empty")]
pub policy: PolicyConfig,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema: Option<SchemaId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub answers: Option<QueueName>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub claims: Option<Predicate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub consumers: Option<Vec<ConsumerName>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub numbered: Option<bool>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PolicyConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub delivery: Option<Delivery>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ordering: Option<Ordering>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub supersede_on: Option<Supersede>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hold_pending: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub blocking_first: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retention: Option<Retention>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub projection: Option<DocumentName>,
}
impl PolicyConfig {
fn is_empty(&self) -> bool {
self == &Self::default()
}
fn apply(&self, policy: &mut Policy) {
if let Some(delivery) = self.delivery {
policy.delivery = delivery;
}
if let Some(ordering) = self.ordering {
policy.ordering = ordering;
}
if let Some(supersede) = &self.supersede_on {
policy.supersede_on = Some(supersede.clone());
}
if let Some(hold) = self.hold_pending {
policy.hold_pending = hold;
}
if let Some(first) = self.blocking_first {
policy.blocking_first = first;
}
if let Some(retention) = self.retention {
policy.retention = retention;
}
if let Some(projection) = &self.projection {
policy.projection = Some(projection.clone());
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct AuthorConfig {
pub capabilities: Vec<OpWord>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub refusals: BTreeMap<OpWord, RefusalReason>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
#[serde(transparent)]
pub struct RefusalReason(#[schemars(regex(pattern = r".*\S.*"))] String);
#[derive(JsonSchema)]
#[schemars(transparent)]
#[expect(
dead_code,
reason = "schema-only mirror constrains configuration map keys without narrowing wire Author"
)]
struct ConfiguredAuthor(#[schemars(regex(pattern = r"^[a-z][a-z0-9-]{0,63}$"))] String);
impl RefusalReason {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl<'de> Deserialize<'de> for RefusalReason {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let reason = String::deserialize(deserializer)?;
if reason.trim().is_empty() {
return Err(serde::de::Error::custom(
"a refusal reason must be non-empty text",
));
}
Ok(Self(reason))
}
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("cannot read the configuration {}: {source}", path.display())]
Read {
path: PathBuf,
source: std::io::Error,
},
#[error("{}: {why}", path.display())]
Parse {
path: PathBuf,
why: String,
},
#[error("version: {found} is not a configuration version this build reads; it reads {CONFIG_VERSION}")]
Version {
found: u32,
},
#[error("profile: `{name}` is not a layout this build links or a linked bundle declares; the layouts are: {known}")]
Profile {
name: String,
known: String,
},
#[error(transparent)]
Narrowing(#[from] NarrowingRefused),
#[error("{key}: {why}")]
Queue {
key: String,
why: String,
},
#[error("transport: {0}")]
Transport(#[from] TransportError),
}
impl Config {
pub fn load(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
let path = path.as_ref();
let text = std::fs::read_to_string(path).map_err(|source| ConfigError::Read {
path: path.to_path_buf(),
source,
})?;
let mut config = Self::parse(&text).map_err(|failure| match failure {
ConfigError::Parse { why, .. } => ConfigError::Parse {
path: path.to_path_buf(),
why,
},
other => other,
})?;
let dir = path.parent().unwrap_or_else(|| Path::new(""));
config.schemas = std::mem::take(&mut config.schemas)
.into_iter()
.map(|link| link.rebased(dir))
.collect();
Ok(config)
}
pub fn parse(text: &str) -> Result<Self, ConfigError> {
let mut options = serde_saphyr::Options::default();
options.strict_booleans = true;
options.with_snippet = false;
let raw: Value =
serde_saphyr::from_str_with_options(text, options.clone()).map_err(|failure| {
ConfigError::Parse {
path: PathBuf::new(),
why: failure.to_string(),
}
})?;
validate_codec_keys(&raw).map_err(|why| ConfigError::Parse {
path: PathBuf::new(),
why,
})?;
validate_author_names(&raw).map_err(|why| ConfigError::Parse {
path: PathBuf::new(),
why,
})?;
let config: Self =
serde_saphyr::from_str_with_options(text, options).map_err(|failure| {
ConfigError::Parse {
path: PathBuf::new(),
why: failure.to_string(),
}
})?;
if config.version != CONFIG_VERSION {
return Err(ConfigError::Version {
found: config.version,
});
}
for (index, validator) in config.validators.iter().enumerate() {
validator.build(index).map_err(|why| ConfigError::Parse {
path: PathBuf::new(),
why,
})?;
}
for (name, codec) in &config.codecs {
crate::codec::validate_codec(name, codec).map_err(|why| ConfigError::Parse {
path: PathBuf::new(),
why,
})?;
}
Ok(config)
}
#[must_use]
pub fn local(dir: impl Into<PathBuf>, profile: Option<&str>) -> Self {
Self {
version: CONFIG_VERSION,
transport: TransportConfig::local(dir),
profile: profile.map(str::to_owned),
queues: BTreeMap::new(),
authors: BTreeMap::new(),
validators: Vec::new(),
codecs: BTreeMap::new(),
schemas: Vec::new(),
}
}
pub fn resolve_links(
&self,
resolver: &LinkResolver,
freshness: Freshness,
) -> Result<Vec<Resolved>, LinkError> {
self.schemas
.iter()
.map(|link| resolver.resolve(link, freshness))
.collect()
}
#[must_use]
pub fn with_transport_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.transport.dir = Some(dir.into());
self
}
pub fn resolve(&self, layouts: &Layouts, kinds: &TransportKinds) -> Result<Bus, ConfigError> {
self.resolve_with_registry(layouts, kinds, &Registry::new())
}
pub fn resolve_with_registry(
&self,
layouts: &Layouts,
kinds: &TransportKinds,
added: &Registry,
) -> Result<Bus, ConfigError> {
self.bind(layouts, added, || Ok(kinds.open(&self.transport)?))
}
pub fn resolve_over(
&self,
layouts: &Layouts,
transport: Arc<dyn Transport>,
added: &Registry,
) -> Result<Bus, ConfigError> {
self.bind(layouts, added, || Ok(transport))
}
fn bind(
&self,
layouts: &Layouts,
added: &Registry,
open: impl FnOnce() -> Result<Arc<dyn Transport>, ConfigError>,
) -> Result<Bus, ConfigError> {
let layout = match &self.profile {
Some(name) => Some(layouts.get(name).ok_or_else(|| ConfigError::Profile {
name: name.clone(),
known: match layouts.names() {
names if names.is_empty() => "(none)".to_owned(),
names => names.join(", "),
},
})?),
None => None,
};
let mut specs: BTreeMap<QueueName, QueueSpec> = layout
.map(|layout| {
layout
.queues()
.into_iter()
.map(|spec| (spec.name.clone(), spec))
.collect()
})
.unwrap_or_default();
for (name, queue) in &self.queues {
let spec = specs
.entry(name.clone())
.or_insert_with(|| QueueSpec::new(name.clone(), Policy::default()));
queue.policy.apply(&mut spec.policy);
if let Some(schema) = &queue.schema {
spec.schema = Some(schema.clone());
}
if let Some(answers) = &queue.answers {
spec.answers = Some(answers.clone());
}
if let Some(claims) = &queue.claims {
spec.claims = Some(claims.clone());
}
if let Some(consumers) = &queue.consumers {
spec.consumers.clone_from(consumers);
}
if let Some(numbered) = queue.numbered {
spec.numbered = numbered;
}
}
let mut registry = layout.map(|layout| layout.registry()).unwrap_or_default();
for id in added.ids() {
if let Some(document) = added.schema(&id) {
registry
.register_schema(id, document.clone())
.map_err(|failure| ConfigError::Queue {
key: "registry".to_owned(),
why: failure.to_string(),
})?;
}
}
for spec in specs.values() {
if let Some(schema) = &spec.schema {
if registry.schema(schema).is_none() {
return Err(ConfigError::Queue {
key: format!("queues.{}.schema", spec.name),
why: format!(
"{schema} is not a schema this layout registers; it registers: {}",
registry
.ids()
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
),
});
}
}
if let Some(answers) = &spec.answers {
if !specs.contains_key(answers) {
return Err(ConfigError::Queue {
key: format!("queues.{}.answers", spec.name),
why: format!("`{answers}` is not a queue this configuration declares"),
});
}
}
}
let mut allowlist = layout
.map(|layout| layout.allowlist())
.unwrap_or_else(|| Allowlist::new(Vec::<OpWord>::new()));
for (author, configured) in &self.authors {
let capabilities_key = format!("authors.{author}.capabilities");
if allowlist.declares(author) {
allowlist.narrow(
&capabilities_key,
author,
&configured.capabilities,
NARROWED,
)?;
} else {
allowlist.declare(author.clone());
for word in &configured.capabilities {
let Some(op) = allowlist
.vocabulary()
.iter()
.find(|op| op == &word)
.cloned()
else {
return Err(NarrowingRefused {
key: capabilities_key.clone(),
why: format!(
"`{}` is not an op; the ops are: {}",
word.0,
allowlist
.vocabulary()
.iter()
.map(|op| op.0.as_str())
.collect::<Vec<_>>()
.join(", ")
),
}
.into());
};
allowlist.grant(author.clone(), op);
}
}
for (word, reason) in &configured.refusals {
let key = format!("authors.{author}.refusals.{}", word.0);
let Some(op) = allowlist
.vocabulary()
.iter()
.find(|op| op == &word)
.cloned()
else {
return Err(NarrowingRefused {
key,
why: format!("`{}` is not an op", word.0),
}
.into());
};
if configured.capabilities.contains(word) {
return Err(NarrowingRefused {
key,
why: "a granted op may not have a refusal".to_owned(),
}
.into());
}
allowlist.refuse(author.clone(), &op, reason.as_str());
}
}
let mut validators: BTreeMap<QueueName, Validators<Value>> = BTreeMap::new();
for (index, declared) in self.validators.iter().enumerate() {
if !specs.contains_key(&declared.on) {
return Err(ConfigError::Queue {
key: format!("validators[{index}].on"),
why: format!(
"`{}` is not a queue this configuration declares; it declares: {}",
declared.on,
specs
.keys()
.map(QueueName::as_str)
.collect::<Vec<_>>()
.join(", ")
),
});
}
let validator = declared.build(index).map_err(|why| ConfigError::Queue {
key: format!("validators[{index}]"),
why,
})?;
validators
.entry(declared.on.clone())
.or_default()
.push(Arc::new(Gated {
when: declared.when.clone(),
validator,
}));
}
let transport = open()?;
Ok(Bus {
transport,
kind: self.transport.kind.to_string(),
layout: layout.cloned(),
queues: specs,
allowlist,
registry: Arc::new(registry),
validators,
})
}
}
fn validate_codec_keys(raw: &Value) -> Result<(), String> {
let Some(codecs) = raw.get("codecs").and_then(Value::as_object) else {
return Ok(());
};
let codec_keys = [
"queue",
"reply_window_seconds",
"session_env",
"asker_env",
"about_env",
"select",
"frames",
];
for (name, codec) in codecs {
let Some(codec) = codec.as_object() else {
continue;
};
if let Some(key) = codec.keys().find(|key| !codec_keys.contains(&key.as_str())) {
return Err(format!("codecs.{name}: unknown field `{key}`"));
}
let Some(frames) = codec.get("frames").and_then(Value::as_object) else {
continue;
};
for (entry, frame) in frames {
let Some(bindings) = frame.get("bindings").and_then(Value::as_array) else {
continue;
};
for (index, binding) in bindings.iter().enumerate() {
let Some(binding) = binding.as_object() else {
continue;
};
let action = binding.get("do").and_then(Value::as_str);
let allowed: &[&str] = match action {
Some("answer") => &["when", "do", "response"],
Some("refuse") => &["when", "do", "message"],
Some("raise") => &["when", "do", "record", "response", "fail"],
Some("ask") => &["when", "do", "record", "blocking", "response", "unanswered"],
_ => &["when", "do"],
};
if let Some(key) = binding.keys().find(|key| !allowed.contains(&key.as_str())) {
return Err(format!(
"codecs.{name}.frames.{entry}.bindings[{index}]: unknown field `{key}`"
));
}
}
}
}
Ok(())
}
fn validate_author_names(raw: &Value) -> Result<(), String> {
let Some(authors) = raw.get("authors").and_then(Value::as_object) else {
return Ok(());
};
let schema = schemars::schema_for!(ConfiguredAuthor).to_value();
let validator = jsonschema::validator_for(&schema)
.expect("the configured-author schema generated by this build is usable");
for name in authors.keys() {
let candidate = Value::String(name.clone());
let failure = validator
.iter_errors(&candidate)
.next()
.map(|failure| failure.to_string());
if let Some(failure) = failure {
return Err(format!("authors.{name}: invalid author name: {failure}"));
}
}
Ok(())
}
pub trait Layout: Send + Sync + 'static {
fn name(&self) -> &str;
fn queues(&self) -> Vec<QueueSpec>;
fn allowlist(&self) -> Allowlist<OpWord>;
fn registry(&self) -> Registry;
fn prepare(
&self,
queue: &QueueName,
record: Value,
allowlist: &Allowlist<OpWord>,
) -> Result<Vec<(QueueName, Value)>, String> {
let _ = allowlist;
Ok(vec![(queue.clone(), record)])
}
}
pub trait Router: Send + Sync {
fn route(
&self,
queue: &QueueName,
record: Value,
allowlist: &Allowlist<OpWord>,
) -> Result<Vec<(QueueName, Value)>, String>;
}
#[derive(Clone, Default)]
pub struct Layouts(Vec<Arc<dyn Layout>>);
impl fmt::Debug for Layouts {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Layouts").field(&self.names()).finish()
}
}
impl Layouts {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with(mut self, layout: Arc<dyn Layout>) -> Self {
self.0.push(layout);
self
}
pub fn with_linked(mut self, linked: &[Resolved]) -> Result<Self, LinkError> {
let mut registry = Registry::new();
for resolved in linked {
resolved.register_into(&mut registry)?;
}
let compiled = self.names();
let mut declared_by: BTreeMap<&str, &Resolved> = BTreeMap::new();
for resolved in linked {
for document in resolved.bundle().layouts() {
if let Some(first) = declared_by.insert(document.name().as_str(), resolved) {
return Err(LinkError::Layout {
link: resolved.link().to_string(),
why: format!(
"`{}` is already declared by {}; a layout is linked once",
document.name(),
first.link()
),
});
}
if compiled.iter().any(|name| name == document.name().as_str()) {
continue;
}
let layout = LinkedLayout::new(document.clone(), ®istry).map_err(|why| {
LinkError::Layout {
link: resolved.link().to_string(),
why: format!("`{}`: {why}", document.name()),
}
})?;
self.0.push(Arc::new(layout));
}
}
Ok(self)
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&Arc<dyn Layout>> {
self.0.iter().find(|layout| layout.name() == name)
}
#[must_use]
pub fn names(&self) -> Vec<String> {
self.0
.iter()
.map(|layout| layout.name().to_owned())
.collect()
}
}
#[derive(Debug, thiserror::Error)]
pub enum BusError {
#[error("`{queue}` is not a queue this configuration declares; it declares: {declared}")]
UnknownQueue {
queue: QueueName,
declared: String,
},
#[error("{queue}: {why}")]
Refused {
queue: QueueName,
why: String,
},
#[error("{queue} is not a queue a question is asked on: {why}")]
NotAskable {
queue: QueueName,
why: String,
},
#[error("{queue}: {why}")]
Unbound {
queue: QueueName,
why: String,
},
#[error(transparent)]
Queue(#[from] QueueError),
}
#[derive(Clone)]
pub struct Bus {
transport: Arc<dyn Transport>,
kind: String,
layout: Option<Arc<dyn Layout>>,
queues: BTreeMap<QueueName, QueueSpec>,
allowlist: Allowlist<OpWord>,
registry: Arc<Registry>,
validators: BTreeMap<QueueName, Validators<Value>>,
}
impl fmt::Debug for Bus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Bus")
.field("kind", &self.kind)
.field("queues", &self.queues.keys().collect::<Vec<_>>())
.finish_non_exhaustive()
}
}
impl Bus {
#[must_use]
pub fn transport(&self) -> &Arc<dyn Transport> {
&self.transport
}
#[must_use]
pub fn kind(&self) -> &str {
&self.kind
}
#[must_use]
pub fn queues(&self) -> Vec<QueueName> {
self.queues.keys().cloned().collect()
}
#[must_use]
pub fn allowlist(&self) -> &Allowlist<OpWord> {
&self.allowlist
}
#[must_use]
pub fn registry(&self) -> &Arc<Registry> {
&self.registry
}
pub fn queue(&self, name: &QueueName) -> Result<RawQueue, BusError> {
let spec = self
.queues
.get(name)
.ok_or_else(|| BusError::UnknownQueue {
queue: name.clone(),
declared: self
.queues
.keys()
.map(QueueName::as_str)
.collect::<Vec<_>>()
.join(", "),
})?;
Ok(RawQueue::open(
Arc::clone(&self.transport),
spec.clone(),
Arc::clone(&self.registry),
)
.with_validators(self.validators.get(name).cloned().unwrap_or_default()))
}
pub fn with_validator<M: Message + 'static>(
mut self,
queue: &QueueName,
validator: impl Validator<M> + 'static,
) -> Result<Self, BusError> {
self.queue(queue)?;
self.validators
.entry(queue.clone())
.or_default()
.push(Arc::new(OnRecords::new(validator)));
Ok(self)
}
pub fn validate(&self, queue: &QueueName, record: Value) -> Result<Verdict, BusError> {
let routed = self.prepare(queue, record.clone())?;
Ok(self.judge(queue, &record, &routed, None))
}
pub(crate) fn judge(
&self,
queue: &QueueName,
offered: &Value,
routed: &[(QueueName, Value)],
correlation: Option<&Correlation>,
) -> Verdict {
let judged_by = |target: &QueueName, record: &Value| {
self.validators.get(target).map_or(Verdict::Pass, |each| {
let context = ValidationContext::new(target.clone());
let context = match correlation {
Some(correlation) => context.with_correlation(correlation.clone()),
None => context,
};
each.judge(record, &context)
})
};
combined(
std::iter::once(judged_by(queue, offered))
.chain(
routed
.iter()
.filter(|(target, _)| target != queue)
.map(|(target, record)| judged_by(target, record)),
)
.collect::<Vec<_>>(),
)
}
pub fn prepare(
&self,
queue: &QueueName,
record: Value,
) -> Result<Vec<(QueueName, Value)>, BusError> {
let spec = self.queue(queue)?.spec().clone();
if (spec.policy.keeps_events() || spec.numbered) && !record.is_object() {
return Err(QueueError::NotAnObject {
queue: queue.clone(),
shape: shape_word(&record),
}
.into());
}
match &self.layout {
Some(layout) => layout
.prepare(queue, record, &self.allowlist)
.map_err(|why| BusError::Refused {
queue: queue.clone(),
why,
}),
None => Ok(vec![(queue.clone(), record)]),
}
}
pub fn send(
&self,
queue: &QueueName,
record: Value,
) -> Result<Vec<(QueueName, Pushed<Value>)>, BusError> {
let routed = self.prepare(queue, record.clone())?;
QueueError::of_verdict(queue, self.judge(queue, &record, &routed, None))?;
let mut pushed = Vec::new();
for (target, record) in routed {
let landed = self.queue(&target)?.push_judged(record)?;
pushed.push((target, landed));
}
Ok(pushed)
}
}