use std::collections::BTreeMap;
use kinjo::{
discovery::{BrowseMode, Entry, EntryGroup, browse_groups},
plumber::{CommandConfig, MatchResult, MatcherBuilder, RuleEngine},
};
struct KeyedEngine {
by_service_type: BTreeMap<String, CommandConfig>,
}
impl KeyedEngine {
fn new(rules: &[(&str, &str)]) -> Self {
let mut builder = MatcherBuilder::new();
builder.start_layer();
for (name, source) in rules {
builder.add_str(name, source).unwrap();
}
let by_service_type = builder
.build()
.commands()
.iter()
.map(|command| (service_type_of(command), command.clone()))
.collect();
Self { by_service_type }
}
}
fn service_type_of(command: &CommandConfig) -> String {
use kinjo::plumber::Predicate;
command
.predicates
.iter()
.find(|p| p.field == "service_type")
.map(|p| match &p.predicate {
Predicate::Equals(value) => value.clone(),
Predicate::Contains(value) => value.clone(),
Predicate::Regex(regex) => regex.as_str().to_string(),
})
.expect("fixture rules all match on service_type")
}
impl RuleEngine for KeyedEngine {
fn matches_group(&self, group: &EntryGroup) -> Vec<MatchResult> {
let instances = group.instances();
let Some(first) = instances.first() else {
return Vec::new();
};
let Some(command) = self.by_service_type.get(&first.service_type) else {
return Vec::new();
};
vec![MatchResult {
command: command.clone(),
targets: instances.to_vec(),
}]
}
fn commands(&self) -> Vec<CommandConfig> {
self.by_service_type.values().cloned().collect()
}
}
fn rule_toml(name: &str, service_type: &str, command: &str) -> String {
format!(
r#"
[metadata]
name = "{name}"
[match.service_type]
equals = "{service_type}"
[action]
command = "{command}"
mode = "execute"
"#
)
}
fn fixture() -> KeyedEngine {
let ssh = rule_toml("ssh", "_ssh._tcp", "ssh -- {hostname}");
let http = rule_toml("http", "_http._tcp", "curl {address}");
KeyedEngine::new(&[("ssh", ssh.as_str()), ("http", http.as_str())])
}
fn entry(name: &str, service_type: &str, address: &str) -> Entry {
let mut record = Entry::new(name, service_type, "local");
record.hostname = Some(format!("{name}.local"));
record.addresses = vec![address.parse().unwrap()];
record.port = Some(22);
record
}
fn group_of(records: &[Entry]) -> EntryGroup {
browse_groups(records, BrowseMode::LogicalService)
.into_iter()
.next()
.expect("fixture always produces one group")
}
#[test]
fn an_engine_without_vec_storage_satisfies_the_trait() {
let engine: Box<dyn RuleEngine> = Box::new(fixture());
let names: Vec<String> = engine
.commands()
.into_iter()
.map(|command| command.name)
.collect();
assert_eq!(names, vec!["http".to_string(), "ssh".to_string()]);
}
#[test]
fn the_default_command_count_agrees_with_commands() {
let engine = fixture();
assert_eq!(engine.command_count(), 2);
assert_eq!(engine.command_count(), engine.commands().len());
}
#[test]
fn a_foreign_strategy_decides_its_own_matches() {
let engine = fixture();
let records = vec![entry("alpha", "_ssh._tcp", "10.0.0.1")];
let matches = engine.matches_group(&group_of(&records));
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].command.name, "ssh");
assert!(
!matches[0].needs_selection(),
"one target, nothing to choose"
);
assert!(
engine
.commands()
.iter()
.any(|command| command.name == matches[0].command.name),
"a match must name a rule the engine also lists"
);
}
#[test]
fn a_foreign_strategy_reports_no_match_for_an_unknown_service_type() {
let engine = fixture();
let records = vec![entry("printer", "_ipp._tcp", "10.0.0.9")];
assert!(engine.matches_group(&group_of(&records)).is_empty());
}
#[test]
fn a_rule_from_a_foreign_engine_prepares_a_command() {
let engine = fixture();
let records = vec![entry("alpha", "_ssh._tcp", "10.0.0.1")];
let matches = engine.matches_group(&group_of(&records));
let prepared = matches[0]
.command
.action
.prepare(&matches[0].targets[0])
.expect("the rule templates only fields the entry has");
assert_eq!(prepared.argv, vec!["ssh", "--", "alpha.local"]);
}
#[test]
fn the_reload_path_accepts_a_foreign_engine() {
use kinjo::ui::app::ReloadOutcome;
let outcome = ReloadOutcome::Loaded(Box::new(fixture()));
match outcome {
ReloadOutcome::Loaded(engine) => assert_eq!(engine.command_count(), 2),
ReloadOutcome::Rejected(diagnostics) => panic!("unexpected rejection: {diagnostics:?}"),
}
}
#[cfg(feature = "fake")]
#[test]
fn a_foreign_engine_composes_into_a_runnable_app() {
use kinjo::{
discovery::{self, DiscoveryBackend},
ui::{
App,
app::ReloadOutcome,
cli::{Cli, CliCommand},
keymap::KeyBindings,
},
};
let cli = Cli {
domain: "local".to_string(),
config_dirs: Vec::new(),
service_type: None,
backend: DiscoveryBackend::Fake,
command: CliCommand::Run,
};
let options = cli
.discovery_options()
.expect("the sample backend honours the default domain");
let session = discovery::start(&options);
let mut app = App::new(cli, fixture(), KeyBindings::default(), session)
.with_discovery_factory(Box::new(move || discovery::start(&options)))
.with_config_loader(Box::new(|_cli| ReloadOutcome::Loaded(Box::new(fixture()))));
let trigger = app.reload_trigger();
assert!(
!trigger.load(std::sync::atomic::Ordering::Relaxed),
"nothing has asked for a reload yet"
);
app.note_skipped_configs(0);
assert!(app.take_reload_diagnostics().is_empty());
}