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::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")]
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<String>,
}
#[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; 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 raw: Value = serde_norway::from_str(text).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,
})?;
let config: Self = serde_norway::from_str(text).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: layouts.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, narrowed) in &self.authors {
allowlist.narrow(
&format!("authors.{author}.capabilities"),
author,
&narrowed.capabilities,
NARROWED,
)?;
}
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(())
}
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
}
#[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)
}
}