use crate::adapter::net::behavior::predicate::{EvalContext, Predicate};
use crate::adapter::net::behavior::tag::{CapabilityTagError, Tag, TagKey, TaxonomyAxis};
#[derive(Debug, Clone, PartialEq)]
pub enum RequiredCapability {
Tag(Tag),
Predicate(Predicate),
AxisAny(TaxonomyAxis),
AxisKey(TagKey),
}
impl RequiredCapability {
pub fn evaluate(&self, ctx: &EvalContext<'_>) -> bool {
match self {
Self::Tag(required) => ctx.tags.iter().any(|t| t.semantic_eq(required)),
Self::Predicate(p) => p.evaluate(ctx),
Self::AxisAny(axis) => ctx.tags.iter().any(|t| t.axis() == Some(*axis)),
Self::AxisKey(key) => ctx.tags.iter().any(|t| t.axis_key().as_ref() == Some(key)),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum RequireParseError {
#[error("require! input must be non-empty")]
Empty,
#[error("require! could not parse tag: {0}")]
Tag(#[from] CapabilityTagError),
#[error("require! numeric value {value:?} for key {key:?} did not parse as f64")]
NumericParse {
key: String,
value: String,
},
#[error("require! tag key {key:?} must be `<axis>.<key>` with a known axis")]
InvalidKey {
key: String,
},
#[error("require_axis! axis {axis:?} is not one of: hardware, software, devices, dataforts")]
InvalidAxis {
axis: String,
},
}
#[doc(hidden)]
pub fn __require_parse(s: &str) -> Result<RequiredCapability, RequireParseError> {
let s = s.trim();
if s.is_empty() {
return Err(RequireParseError::Empty);
}
if let Some((lhs, rhs)) = s.split_once("==") {
let lhs = lhs.trim();
let rhs = rhs.trim().trim_matches('"');
let key = parse_tag_key(lhs)?;
return Ok(RequiredCapability::Predicate(Predicate::equals(
key,
rhs.to_string(),
)));
}
for (op, build) in [
(
">=",
(|key: TagKey, n: f64| Predicate::numeric_at_least(key, n))
as fn(TagKey, f64) -> Predicate,
),
(
"<=",
(|key: TagKey, n: f64| Predicate::numeric_at_most(key, n))
as fn(TagKey, f64) -> Predicate,
),
] {
if let Some((lhs, rhs)) = s.split_once(op) {
let lhs = lhs.trim();
let rhs = rhs.trim();
let key = parse_tag_key(lhs)?;
let n: f64 = rhs.parse().map_err(|_| RequireParseError::NumericParse {
key: lhs.to_string(),
value: rhs.to_string(),
})?;
return Ok(RequiredCapability::Predicate(build(key, n)));
}
}
let tag = Tag::parse_user(s)?;
Ok(RequiredCapability::Tag(tag))
}
#[doc(hidden)]
pub fn __require_axis_parse(s: &str) -> Result<TaxonomyAxis, RequireParseError> {
TaxonomyAxis::from_prefix(s.trim()).ok_or_else(|| RequireParseError::InvalidAxis {
axis: s.to_string(),
})
}
#[doc(hidden)]
pub fn __require_axis_value_parse(axis: &str, key: &str) -> Result<TagKey, RequireParseError> {
let axis =
TaxonomyAxis::from_prefix(axis.trim()).ok_or_else(|| RequireParseError::InvalidAxis {
axis: axis.to_string(),
})?;
let key = key.trim();
if key.is_empty() {
return Err(RequireParseError::InvalidKey { key: String::new() });
}
Ok(TagKey::new(axis, key))
}
fn parse_tag_key(s: &str) -> Result<TagKey, RequireParseError> {
let (axis_str, key) = s
.split_once('.')
.ok_or_else(|| RequireParseError::InvalidKey { key: s.to_string() })?;
let axis_str = axis_str.trim();
let key = key.trim();
let axis = TaxonomyAxis::from_prefix(axis_str)
.ok_or_else(|| RequireParseError::InvalidKey { key: s.to_string() })?;
if key.is_empty() {
return Err(RequireParseError::InvalidKey { key: s.to_string() });
}
Ok(TagKey::new(axis, key.to_string()))
}
#[macro_export]
macro_rules! require {
($spec:literal) => {
$crate::adapter::net::behavior::required_capability::__require_parse($spec)
.unwrap_or_else(|e| panic!("require!({:?}) failed at parse time: {}", $spec, e))
};
}
#[macro_export]
macro_rules! require_axis {
($axis:literal) => {
$crate::adapter::net::behavior::required_capability::RequiredCapability::AxisAny(
$crate::adapter::net::behavior::required_capability::__require_axis_parse($axis)
.unwrap_or_else(|e| panic!("require_axis!({:?}) failed: {}", $axis, e)),
)
};
}
#[macro_export]
macro_rules! require_axis_value {
($axis:literal, $key:literal) => {
$crate::adapter::net::behavior::required_capability::RequiredCapability::AxisKey(
$crate::adapter::net::behavior::required_capability::__require_axis_value_parse(
$axis, $key,
)
.unwrap_or_else(|e| {
panic!("require_axis_value!({:?}, {:?}) failed: {}", $axis, $key, e)
}),
)
};
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::*;
use crate::adapter::net::behavior::tag::{AxisSeparator, Tag, TaxonomyAxis};
fn axis_present(axis: TaxonomyAxis, key: &str) -> Tag {
Tag::AxisPresent {
axis,
key: key.into(),
}
}
fn axis_eq(axis: TaxonomyAxis, key: &str, value: &str) -> Tag {
Tag::AxisValue {
axis,
key: key.into(),
value: value.into(),
separator: AxisSeparator::Eq,
}
}
fn axis_colon(axis: TaxonomyAxis, key: &str, value: &str) -> Tag {
Tag::AxisValue {
axis,
key: key.into(),
value: value.into(),
separator: AxisSeparator::Colon,
}
}
fn meta() -> BTreeMap<String, String> {
BTreeMap::new()
}
#[test]
fn require_axis_presence() {
let r = require!("hardware.gpu");
assert_eq!(
r,
RequiredCapability::Tag(Tag::AxisPresent {
axis: TaxonomyAxis::Hardware,
key: "gpu".into(),
})
);
}
#[test]
fn require_axis_value_eq() {
let r = require!("hardware.gpu.vram_gb=80");
assert_eq!(
r,
RequiredCapability::Tag(Tag::AxisValue {
axis: TaxonomyAxis::Hardware,
key: "gpu.vram_gb".into(),
value: "80".into(),
separator: AxisSeparator::Eq,
})
);
}
#[test]
fn require_dataforts_pre_typed_colon() {
let r = require!("software.daemon:postgres");
match r {
RequiredCapability::Tag(Tag::AxisValue {
axis,
key,
value,
separator,
}) => {
assert_eq!(axis, TaxonomyAxis::Software);
assert_eq!(key, "daemon");
assert_eq!(value, "postgres");
assert_eq!(separator, AxisSeparator::Colon);
}
other => panic!("expected AxisValue with `:` separator, got {other:?}"),
}
}
#[test]
fn require_numeric_at_least() {
let r = require!("hardware.gpu.vram_gb >= 24");
match r {
RequiredCapability::Predicate(Predicate::NumericAtLeast { key, threshold }) => {
assert_eq!(key.axis, TaxonomyAxis::Hardware);
assert_eq!(key.key, "gpu.vram_gb");
assert!((threshold - 24.0).abs() < f64::EPSILON);
}
other => panic!("expected NumericAtLeast, got {other:?}"),
}
}
#[test]
fn require_numeric_at_most() {
let r = require!("hardware.cpu_cores <= 64");
match r {
RequiredCapability::Predicate(Predicate::NumericAtMost { key, threshold }) => {
assert_eq!(key.key, "cpu_cores");
assert!((threshold - 64.0).abs() < f64::EPSILON);
}
other => panic!("expected NumericAtMost, got {other:?}"),
}
}
#[test]
fn require_numeric_threshold_can_be_float() {
let r = require!("hardware.cpu_cores >= 1.5");
match r {
RequiredCapability::Predicate(Predicate::NumericAtLeast { threshold, .. }) => {
assert!((threshold - 1.5).abs() < f64::EPSILON);
}
other => panic!("expected NumericAtLeast, got {other:?}"),
}
}
#[test]
fn require_string_equality() {
let r = require!("software.runtime == \"cuda-12.4\"");
match r {
RequiredCapability::Predicate(Predicate::Equals { key, value }) => {
assert_eq!(key.axis, TaxonomyAxis::Software);
assert_eq!(key.key, "runtime");
assert_eq!(value, "cuda-12.4");
}
other => panic!("expected Equals, got {other:?}"),
}
}
#[test]
fn require_equality_value_containing_ge_is_not_claimed_by_numeric_branch() {
let r = require!("software.id == v>=1.0");
match r {
RequiredCapability::Predicate(Predicate::Equals { key, value }) => {
assert_eq!(key.axis, TaxonomyAxis::Software);
assert_eq!(key.key, "id");
assert_eq!(value, "v>=1.0");
}
other => panic!(
"expected Equals(software.id, v>=1.0), got {other:?} \
— `==` should bind tighter than `>=`"
),
}
}
#[test]
fn require_parse_tag_key_trims_whitespace_around_dot() {
let r = require!("hardware. gpu == nvidia");
match r {
RequiredCapability::Predicate(Predicate::Equals { key, value }) => {
assert_eq!(key.axis, TaxonomyAxis::Hardware);
assert_eq!(key.key, "gpu", "key must not carry leading whitespace");
assert_eq!(value, "nvidia");
}
other => panic!("expected Equals(hardware.gpu, nvidia), got {other:?}"),
}
let r = require!(" hardware .gpu == nvidia");
match r {
RequiredCapability::Predicate(Predicate::Equals { key, value: _ }) => {
assert_eq!(key.axis, TaxonomyAxis::Hardware);
assert_eq!(key.key, "gpu");
}
other => panic!("expected Equals on hardware axis, got {other:?}"),
}
}
#[test]
fn require_axis_each_taxonomy() {
for axis in TaxonomyAxis::all() {
let r = match axis {
TaxonomyAxis::Hardware => require_axis!("hardware"),
TaxonomyAxis::Software => require_axis!("software"),
TaxonomyAxis::Devices => require_axis!("devices"),
TaxonomyAxis::Dataforts => require_axis!("dataforts"),
};
assert_eq!(r, RequiredCapability::AxisAny(axis));
}
}
#[test]
fn require_axis_value_basic() {
let r = require_axis_value!("software", "model");
assert_eq!(
r,
RequiredCapability::AxisKey(TagKey::new(TaxonomyAxis::Software, "model"))
);
}
#[test]
fn tag_variant_matches_exact_tag() {
let tags = [axis_present(TaxonomyAxis::Hardware, "gpu")];
let m = meta();
let ctx = EvalContext::new(&tags, &m);
let r = require!("hardware.gpu");
assert!(r.evaluate(&ctx));
let r = require!("hardware.tpu");
assert!(!r.evaluate(&ctx));
}
#[test]
fn tag_variant_value_matches_exactly() {
let tags = [axis_eq(TaxonomyAxis::Hardware, "gpu.vram_gb", "80")];
let m = meta();
let ctx = EvalContext::new(&tags, &m);
let r = require!("hardware.gpu.vram_gb=80");
assert!(r.evaluate(&ctx));
let r = require!("hardware.gpu.vram_gb=24");
assert!(!r.evaluate(&ctx));
}
#[test]
fn tag_variant_evaluates_across_separator_forms() {
let m = meta();
let tags = [axis_colon(TaxonomyAxis::Software, "os", "linux")];
let ctx = EvalContext::new(&tags, &m);
let r = require!("software.os=linux");
assert!(r.evaluate(&ctx));
let tags = [axis_eq(TaxonomyAxis::Software, "os", "linux")];
let ctx = EvalContext::new(&tags, &m);
let r = require!("software.os:linux");
assert!(r.evaluate(&ctx));
let r = require!("software.os:darwin");
assert!(!r.evaluate(&ctx));
}
#[test]
fn predicate_variant_evaluates_via_predicate() {
let tags = [axis_eq(TaxonomyAxis::Hardware, "gpu.vram_gb", "80")];
let m = meta();
let ctx = EvalContext::new(&tags, &m);
let r = require!("hardware.gpu.vram_gb >= 24");
assert!(r.evaluate(&ctx));
let r = require!("hardware.gpu.vram_gb >= 96");
assert!(!r.evaluate(&ctx));
}
#[test]
fn axis_any_matches_any_tag_in_axis() {
let tags = [axis_present(TaxonomyAxis::Devices, "lidar")];
let m = meta();
let ctx = EvalContext::new(&tags, &m);
let r = require_axis!("devices");
assert!(r.evaluate(&ctx));
let tags = [axis_present(TaxonomyAxis::Hardware, "gpu")];
let ctx = EvalContext::new(&tags, &m);
assert!(!r.evaluate(&ctx));
}
#[test]
fn axis_key_matches_presence_or_value() {
let tags = [axis_present(TaxonomyAxis::Software, "model")];
let m = meta();
let ctx = EvalContext::new(&tags, &m);
let r = require_axis_value!("software", "model");
assert!(r.evaluate(&ctx));
let tags = [axis_colon(TaxonomyAxis::Software, "model", "llama-7b")];
let ctx = EvalContext::new(&tags, &m);
assert!(r.evaluate(&ctx));
let tags = [axis_present(TaxonomyAxis::Software, "runtime")];
let ctx = EvalContext::new(&tags, &m);
assert!(!r.evaluate(&ctx));
}
#[test]
fn require_unknown_axis_falls_through_to_legacy_tag() {
let r = __require_parse("bogus.foo").unwrap();
match r {
RequiredCapability::Tag(Tag::Legacy(s)) => assert_eq!(s, "bogus.foo"),
other => panic!("expected Tag(Legacy(...)), got {other:?}"),
}
}
#[test]
fn require_parses_unparseable_threshold_as_error() {
match __require_parse("hardware.cpu_cores >= many") {
Err(RequireParseError::NumericParse { key, value }) => {
assert_eq!(key, "hardware.cpu_cores");
assert_eq!(value, "many");
}
other => panic!("expected NumericParse error, got {other:?}"),
}
}
#[test]
fn require_rejects_reserved_prefix() {
match __require_parse("scope:prod") {
Err(RequireParseError::Tag(CapabilityTagError::ReservedPrefix { prefix, .. })) => {
assert_eq!(prefix, "scope:");
}
other => panic!("expected ReservedPrefix, got {other:?}"),
}
}
#[test]
fn require_rejects_empty() {
match __require_parse("") {
Err(RequireParseError::Empty) => {}
other => panic!("expected Empty, got {other:?}"),
}
match __require_parse(" ") {
Err(RequireParseError::Empty) => {}
other => panic!("expected Empty, got {other:?}"),
}
}
#[test]
fn require_axis_rejects_unknown_axis() {
match __require_axis_parse("bogus") {
Err(RequireParseError::InvalidAxis { axis }) => {
assert_eq!(axis, "bogus");
}
other => panic!("expected InvalidAxis, got {other:?}"),
}
}
#[test]
fn require_axis_value_rejects_empty_key() {
match __require_axis_value_parse("software", "") {
Err(RequireParseError::InvalidKey { .. }) => {}
other => panic!("expected InvalidKey, got {other:?}"),
}
}
#[test]
fn intent_registry_defaults_examples_compile_and_evaluate() {
let reqs = [
require!("hardware.gpu"),
require!("hardware.gpu.vram_gb >= 24"),
];
let tags = [
axis_present(TaxonomyAxis::Hardware, "gpu"),
axis_eq(TaxonomyAxis::Hardware, "gpu.vram_gb", "80"),
];
let m = meta();
let ctx = EvalContext::new(&tags, &m);
assert!(reqs.iter().all(|r| r.evaluate(&ctx)));
let tags = [
axis_present(TaxonomyAxis::Hardware, "gpu"),
axis_eq(TaxonomyAxis::Hardware, "gpu.vram_gb", "16"),
];
let ctx = EvalContext::new(&tags, &m);
assert!(!reqs.iter().all(|r| r.evaluate(&ctx)));
let tags: Vec<Tag> = vec![];
let ctx = EvalContext::new(&tags, &m);
assert!(!reqs.iter().any(|r| r.evaluate(&ctx)));
}
}