#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WireTarget {
Property(crate::widget::capability::types::PropertyValueKind),
PayloadFreeCommand,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WireCompatibility {
Direct,
Converted,
Rejected(&'static str),
}
impl WireCompatibility {
pub fn is_offered(self) -> bool {
!matches!(self, Self::Rejected(_))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WireRule {
pub source: Option<crate::widget::capability::types::PropertyValueKind>,
pub target: crate::widget::capability::types::PropertyValueKind,
pub verdict: WireCompatibility,
pub reason: &'static str,
}
use crate::widget::capability::types::PropertyValueKind as K;
pub const WIRE_RULES: &[WireRule] = &[
rule(Some(K::Bool), K::Bool, Direct, "the event's boolean is already the property's type"),
rule(Some(K::Int), K::Int, Direct, "the event's integer is already the property's type"),
rule(Some(K::UInt), K::UInt, Direct, "the event's integer is already the property's type"),
rule(Some(K::Float), K::Float, Direct, "the event's number is already the property's type"),
rule(
Some(K::Number),
K::Number,
Direct,
"both sides carry a number; the target picks Int or Float",
),
rule(Some(K::String), K::String, Direct, "the event's text is already the property's type"),
rule(Some(K::Color), K::Color, Direct, "the event's colour is already the property's type"),
rule(Some(K::Rect), K::Rect, Direct, "the event's rectangle is already the property's type"),
rule(Some(K::Enum), K::Enum, Direct, "both sides use the same enumerated token"),
rule(Some(K::Int), K::UInt, Direct, "an integer fits an unsigned property in this range"),
rule(Some(K::UInt), K::Int, Direct, "an unsigned integer fits a signed property"),
rule(Some(K::Int), K::Float, Direct, "an integer is exactly representable as a number"),
rule(
Some(K::UInt),
K::Float,
Direct,
"an unsigned integer is exactly representable as a number",
),
rule(Some(K::Int), K::Number, Direct, "a whole number is a number the control can carry"),
rule(Some(K::UInt), K::Number, Direct, "a whole number is a number the control can carry"),
rule(Some(K::Float), K::Number, Direct, "a number is the property's own type"),
rule(Some(K::Bool), K::String, Converted, "a boolean is formatted as text for display"),
rule(Some(K::Int), K::String, Converted, "an integer is formatted as text for display"),
rule(
Some(K::UInt),
K::String,
Converted,
"an unsigned integer is formatted as text for display",
),
rule(Some(K::Float), K::String, Converted, "a number is formatted as text for display"),
rule(Some(K::Number), K::String, Converted, "a number is formatted as text for display"),
rule(Some(K::Color), K::String, Converted, "a colour is written as its `#rrggbbaa` token"),
rule(Some(K::Rect), K::String, Converted, "a rectangle is written as its `x,y,w,h` token"),
rule(Some(K::Enum), K::String, Converted, "an enumerated token is already text"),
];
pub fn payload_free_command_verdict() -> WireCompatibility {
WireCompatibility::Direct
}
const fn rule(
source: Option<K>,
target: K,
verdict: WireCompatibility,
reason: &'static str,
) -> WireRule {
WireRule { source, target, verdict, reason }
}
pub fn compatibility(
source: Option<crate::widget::capability::types::PropertyValueKind>,
target: WireTarget,
) -> WireCompatibility {
match target {
WireTarget::PayloadFreeCommand => match source {
None => payload_free_command_verdict(),
Some(kind) => WireCompatibility::Rejected(payload_free_command_reason(kind)),
},
WireTarget::Property(target_kind) => {
for candidate in WIRE_RULES {
if candidate.source == source && candidate.target == target_kind {
return candidate.verdict;
}
}
WireCompatibility::Rejected(rejection_reason(source, target_kind))
}
}
}
fn payload_free_command_reason(
_kind: crate::widget::capability::types::PropertyValueKind,
) -> &'static str {
"this command takes no payload, so a value-carrying event has nowhere to put its value; \
use a property target or a command that accepts one"
}
fn rejection_reason(
source: Option<crate::widget::capability::types::PropertyValueKind>,
target: crate::widget::capability::types::PropertyValueKind,
) -> &'static str {
use crate::widget::capability::types::PropertyValueKind as Kind;
match (source, target) {
(Some(Kind::String), Kind::Int | Kind::UInt | Kind::Float | Kind::Number) => {
"text cannot be assigned to a number: parsing can fail, and a failed parse has no \
defined result here; add an explicit conversion"
}
(Some(_), _) => {
"this event's value type does not match the property's type, and no conversion is \
defined for the pair"
}
(None, _) => {
"this event carries no value, so it cannot assign a property; target a \
payload-free command instead"
}
}
}
use WireCompatibility::{Converted, Direct};
#[cfg(test)]
mod tests {
use super::*;
use crate::widget::capability::types::PropertyValueKind as Kind;
use WireCompatibility::Rejected;
fn prop(kind: Kind) -> WireTarget {
WireTarget::Property(kind)
}
#[test]
fn a_matching_kind_is_direct() {
for kind in [
Kind::Bool,
Kind::Int,
Kind::UInt,
Kind::Float,
Kind::Number,
Kind::String,
Kind::Color,
Kind::Rect,
Kind::Enum,
] {
assert_eq!(
compatibility(Some(kind), prop(kind)),
Direct,
"{kind:?} → {kind:?} must be offered"
);
}
}
#[test]
fn a_number_property_accepts_both_numeric_carriers() {
for source in [Kind::Int, Kind::UInt, Kind::Float] {
assert_eq!(
compatibility(Some(source), prop(Kind::Number)),
Direct,
"{source:?} → Number must be offered; the control picks the carrier"
);
}
assert!(matches!(compatibility(Some(Kind::String), prop(Kind::Number)), Rejected(_)));
}
#[test]
fn a_number_can_be_displayed_as_text() {
for kind in [Kind::Int, Kind::UInt, Kind::Float, Kind::Number] {
assert_eq!(
compatibility(Some(kind), prop(Kind::String)),
Converted,
"{kind:?} → text is a formatting step, not a rejection"
);
}
}
#[test]
fn text_into_a_number_is_rejected_with_that_reason() {
for target in [Kind::Int, Kind::UInt, Kind::Float, Kind::Number] {
let verdict = compatibility(Some(Kind::String), prop(target));
match verdict {
Rejected(reason) => assert!(
reason.contains("parsing can fail"),
"the rejection must name the actual hazard, got: {reason}"
),
other => panic!("text → {target:?} must be rejected, got {other:?}"),
}
assert!(!verdict.is_offered());
}
}
#[test]
fn a_payload_free_event_cannot_assign_a_property() {
for target in [
Kind::Bool,
Kind::Int,
Kind::UInt,
Kind::Float,
Kind::Number,
Kind::String,
Kind::Color,
Kind::Rect,
Kind::Enum,
] {
let verdict = compatibility(None, prop(target));
assert!(
matches!(verdict, Rejected(_)),
"a payload-free event carries no value, so it cannot assign {target:?}"
);
}
assert_eq!(compatibility(None, WireTarget::PayloadFreeCommand), Direct);
}
#[test]
fn a_value_carrying_event_cannot_start_a_payload_free_command() {
let verdict = compatibility(Some(Kind::Int), WireTarget::PayloadFreeCommand);
match verdict {
Rejected(reason) => {
assert!(reason.contains("no payload"), "the reason must say why, got: {reason}")
}
other => {
panic!("a payload-carrying event cannot feed a payload-free command: {other:?}")
}
}
assert_eq!(compatibility(None, WireTarget::PayloadFreeCommand), Direct);
}
#[test]
fn an_unlisted_pair_is_rejected_not_assumed_compatible() {
assert!(matches!(compatibility(Some(Kind::Rect), prop(Kind::Bool)), Rejected(_)));
assert!(matches!(compatibility(Some(Kind::Bool), prop(Kind::Rect)), Rejected(_)));
}
#[test]
fn every_rule_states_a_reason() {
for rule in WIRE_RULES {
assert!(!rule.reason.trim().is_empty(), "rule {rule:?} has no reason");
}
}
#[test]
fn the_rule_table_has_no_duplicate_pairs() {
for (index, first) in WIRE_RULES.iter().enumerate() {
for second in &WIRE_RULES[index + 1..] {
assert!(
!(first.source == second.source && first.target == second.target),
"two rules describe {:?} → {:?}; the later one is unreachable",
first.source,
second.target
);
}
}
}
}