use std::any::Any;
use crate::cap::Cap;
use crate::ecs::{AdmittedInput, identify_output, same_output};
use crate::effect::InputIdentity;
use crate::error::BotError;
use crate::spec::{EvaluateAny, ExecuteAny, ObserveAny, TypedEval, TypedExec, Witness};
use crate::verb::{Evaluate, Execute, Observe};
pub type SourceCtor = fn(&str) -> Result<Source, BotError>;
pub type ActionCtor = fn(&str) -> Result<Action, BotError>;
pub struct Source {
inner: Box<dyn ObserveAny>,
same: fn(&dyn Any, &dyn Any) -> bool,
identify: fn(&dyn Any) -> AdmittedInput,
witness: Witness,
condition: fn(&str) -> Result<Condition, BotError>,
}
impl Source {
#[must_use]
pub fn new<O>(source: O) -> Self
where
O: Observe + 'static,
O::Output: Clone + PartialEq + InputIdentity + 'static,
{
Self::erase(source, make_condition::<O>)
}
#[must_use]
pub fn ordered<O>(source: O) -> Self
where
O: Observe + 'static,
O::Output: Clone + PartialEq + PartialOrd + std::str::FromStr + InputIdentity + 'static,
<O::Output as std::str::FromStr>::Err: std::fmt::Display,
{
Self::erase(source, make_ordered_condition::<O>)
}
fn erase<O>(source: O, condition: fn(&str) -> Result<Condition, BotError>) -> Self
where
O: Observe + 'static,
O::Output: Clone + PartialEq + InputIdentity + 'static,
{
Self {
inner: Box::new(source),
same: same_output::<O>,
identify: identify_output::<O>,
witness: Witness::of::<O::Output>(),
condition,
}
}
#[must_use]
pub fn domain_id(&self) -> &str {
self.inner.domain_id()
}
#[must_use]
pub fn required_caps(&self) -> &[Cap] {
self.inner.required_caps()
}
pub fn condition(&self, condition_id: &str) -> Result<Condition, BotError> {
(self.condition)(condition_id)
}
pub(crate) fn into_parts(self) -> SourceParts {
(self.inner, self.same, self.identify, self.witness)
}
}
pub(crate) type SourceParts = (
Box<dyn ObserveAny>,
fn(&dyn Any, &dyn Any) -> bool,
fn(&dyn Any) -> AdmittedInput,
Witness,
);
fn make_condition<O>(condition_id: &str) -> Result<Condition, BotError>
where
O: Observe + 'static,
O::Output: Clone + PartialEq + 'static,
{
let identifier = condition_id.trim();
match identifier {
"changed" => Ok(Condition::new::<O::Output, _>(
crate::domain::eval::Changed::new(),
)),
"always" => Ok(Condition::new::<O::Output, _>(|_: &O::Output| true)),
_ => Err(BotError::UnknownCondition {
condition: identifier.to_owned(),
argument_parse: String::from("not an identifier this build knows"),
}),
}
}
fn make_ordered_condition<O>(condition_id: &str) -> Result<Condition, BotError>
where
O: Observe + 'static,
O::Output: Clone + PartialEq + PartialOrd + std::str::FromStr + 'static,
<O::Output as std::str::FromStr>::Err: std::fmt::Display,
{
let identifier = condition_id.trim();
if let Some(argument) = parenthesized(identifier, "threshold::above") {
let bound = parse_bound::<O::Output>(argument, identifier)?;
return Ok(Condition::new::<O::Output, _>(
crate::domain::eval::Above::new(bound),
));
}
if let Some(argument) = parenthesized(identifier, "threshold::below") {
let bound = parse_bound::<O::Output>(argument, identifier)?;
return Ok(Condition::new::<O::Output, _>(
crate::domain::eval::Below::new(bound),
));
}
make_condition::<O>(identifier)
}
fn parenthesized<'a>(identifier: &'a str, prefix: &str) -> Option<&'a str> {
identifier
.strip_prefix(prefix)?
.strip_prefix('(')?
.strip_suffix(')')
}
fn parse_bound<T>(argument: &str, identifier: &str) -> Result<T, BotError>
where
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
argument
.trim()
.parse::<T>()
.map_err(|not_this_type| BotError::UnknownCondition {
condition: identifier.to_owned(),
argument_parse: not_this_type.to_string(),
})
}
impl std::fmt::Debug for Source {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_tuple("Source")
.field(&self.domain_id())
.finish()
}
}
pub struct Action(Box<dyn ExecuteAny>);
impl Action {
#[must_use]
pub fn new<A>(action: A) -> Self
where
A: Execute + 'static,
A::Input: 'static,
A::Output: 'static,
{
Self(Box::new(TypedExec::new(action)))
}
#[must_use]
pub fn domain_id(&self) -> &str {
self.0.domain_id()
}
#[must_use]
pub fn required_caps(&self) -> &[Cap] {
self.0.required_caps()
}
pub(crate) fn into_execute_any(self) -> Box<dyn ExecuteAny> {
self.0
}
}
impl std::fmt::Debug for Action {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_tuple("Action")
.field(&self.domain_id())
.finish()
}
}
pub struct Condition(Box<dyn EvaluateAny>);
impl Condition {
#[must_use]
pub fn new<T, C>(condition: C) -> Self
where
T: 'static,
C: Evaluate<T> + 'static,
{
Self(Box::new(TypedEval {
inner: condition,
_marker: std::marker::PhantomData::<T>,
}))
}
pub(crate) fn into_evaluate_any(self) -> Box<dyn EvaluateAny> {
self.0
}
}
impl std::fmt::Debug for Condition {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("Condition")
}
}
pub struct DomainRegistry {
sources: &'static [(&'static str, SourceCtor)],
actions: &'static [(&'static str, ActionCtor)],
}
impl DomainRegistry {
#[must_use]
pub const fn new(
sources: &'static [(&'static str, SourceCtor)],
actions: &'static [(&'static str, ActionCtor)],
) -> Self {
Self { sources, actions }
}
#[must_use]
pub const fn empty() -> Self {
Self {
sources: &[],
actions: &[],
}
}
#[must_use]
pub fn source(&self, domain_id: &str) -> Option<SourceCtor> {
find(self.sources, domain_id)
}
#[must_use]
pub fn action(&self, domain_id: &str) -> Option<ActionCtor> {
find(self.actions, domain_id)
}
pub fn validate(&self) -> Result<(), BotError> {
if let Some((first, second)) = first_duplicate(self.sources) {
let refusal = Err(BotError::DuplicateDomain {
domain: self.sources[first].0.to_owned(),
role: "source",
first,
second,
});
lgwks_std::trace::debug!(error = ?refusal.as_ref().err(), "validate: returning an error to the caller");
return refusal;
}
if let Some((first, second)) = first_duplicate(self.actions) {
let refusal = Err(BotError::DuplicateDomain {
domain: self.actions[first].0.to_owned(),
role: "action",
first,
second,
});
lgwks_std::trace::debug!(error = ?refusal.as_ref().err(), "validate: returning an error to the caller");
return refusal;
}
Ok(())
}
pub fn build_source(&self, domain_id: &str, target: &str) -> Result<Source, BotError> {
self.validate()?;
match self.source(domain_id) {
Some(ctor) => ctor(target),
None => Err(BotError::UnregisteredDomain {
domain: domain_id.to_owned(),
}),
}
}
pub fn build_action(&self, domain_id: &str, target: &str) -> Result<Action, BotError> {
self.validate()?;
match self.action(domain_id) {
Some(ctor) => ctor(target),
None => Err(BotError::UnregisteredDomain {
domain: domain_id.to_owned(),
}),
}
}
pub fn source_ids(&self) -> impl Iterator<Item = &'static str> + '_ {
self.sources.iter().map(|&(domain_id, _)| domain_id)
}
pub fn action_ids(&self) -> impl Iterator<Item = &'static str> + '_ {
self.actions.iter().map(|&(domain_id, _)| domain_id)
}
}
impl std::fmt::Debug for DomainRegistry {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("DomainRegistry")
.field("sources", &self.source_ids().collect::<Vec<&str>>())
.field("actions", &self.action_ids().collect::<Vec<&str>>())
.finish()
}
}
fn find<T: Copy>(entries: &[(&'static str, T)], domain_id: &str) -> Option<T> {
let mut found: Option<T> = None;
for &(registered, ctor) in entries {
if registered == domain_id {
if found.is_some() {
return None;
}
found = Some(ctor);
}
}
found
}
fn first_duplicate<T: PartialEq>(entries: &[(&'static str, T)]) -> Option<(usize, usize)> {
for (first, &(registered, _)) in entries.iter().enumerate() {
for (second, &(other, _)) in entries.iter().enumerate() {
if second > first && other == registered {
return Some((first, second));
}
}
}
None
}
#[macro_export]
macro_rules! domains {
(
$(#[$meta:meta])*
$vis:vis $name:ident {
observe { $( $source_id:literal => $source_ctor:path ),* $(,)? }
execute { $( $action_id:literal => $action_ctor:path ),* $(,)? }
}
) => {
$(#[$meta])*
$vis static $name: $crate::DomainRegistry = $crate::DomainRegistry::new(
&[ $( ($source_id, $source_ctor as $crate::SourceCtor) ),* ],
&[ $( ($action_id, $action_ctor as $crate::ActionCtor) ),* ],
);
};
}