i suggested the Banger. copilot worked it good

This commit is contained in:
lda committed 2026-04-14 01:57:38 +07:00
1 parent 5251ad2a9a
commit 458123428e
12 files changed
+202 -171

No files matched your search

+13 -25
View File
@@ -8,7 +8,7 @@ use anyhow::Result;
use clap::{ArgAction, Args, Parser, Subcommand};
use tracing::debug;
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
use wakey_core::{DeviceFilters, DeviceQuery, InterfaceSummary, WakeResult};
use wakey_core::{InterfaceSummary, InventoryQuery, InventoryQueryBuilder, Query, WakeResult};
#[derive(Parser)]
#[command(name = "wakey")]
@@ -141,7 +141,10 @@ pub async fn run(cli: Cli) -> Result<()> {
Command::Inventory(args) => {
let as_json = args.json;
let query = inventory_args_to_query(args);
let selected_name = query.name.clone();
let selected_name = query.iter().find_map(|term| match term {
Query::Text(text) => Some(text.clone()),
_ => None,
});
debug!(?query, json = as_json, "dispatching inventory command");
let status = wakey::inventory(query).await?;
if as_json {
@@ -207,29 +210,14 @@ pub async fn run(cli: Cli) -> Result<()> {
Ok(())
}
fn inventory_args_to_query(args: InventoryArgs) -> DeviceQuery {
if let Some(query) = args.query.as_ref()
&& args.name.is_none()
&& args.ips.is_empty()
&& args.devs.is_empty()
&& args.nuds.is_empty()
&& args.macs.is_empty()
{
return DeviceQuery {
name: Some(query.clone()),
..Default::default()
};
}
DeviceQuery {
name: args.name.or(args.query),
filter: DeviceFilters {
ips: args.ips,
devs: args.devs,
nuds: args.nuds,
macs: args.macs,
},
}
fn inventory_args_to_query(args: InventoryArgs) -> InventoryQuery {
InventoryQueryBuilder::new()
.maybe_text(args.name.or(args.query))
.ips(args.ips)
.interfaces(args.devs)
.neighbor_states(args.nuds)
.macs(args.macs)
.build()
}
fn validate_wake_args(args: &WakeArgs) -> Result<()> {
+9 -6
View File
@@ -3,7 +3,7 @@ pub mod utils;
pub use service::{
broadcast_wake_targets, get_interface_summaries, get_interface_summary, get_ips, get_leases,
inventory, leases_without_state, merge_devices, query_to_device_query, resolve_devices,
inventory, leases_without_state, merge_devices, query_to_inventory_query, resolve_devices,
resolve_query, resolve_selector, resolve_wake_targets, wake_explicit, wake_from_query,
wake_targets,
};
@@ -20,8 +20,8 @@ mod tests {
async fn resolve_query_parses_ip() {
let query = resolve_query("192.168.1.10").await.expect("resolve query");
assert_eq!(
query.filter.ips,
vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10))]
query,
vec![Query::Ip(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)))]
);
}
@@ -30,13 +30,16 @@ mod tests {
let query = resolve_query("aa:bb:cc:dd:ee:ff")
.await
.expect("resolve query");
assert_eq!(query.filter.macs.len(), 1);
assert_eq!(
query,
vec![Query::Mac("aa:bb:cc:dd:ee:ff".parse().expect("mac"))]
);
}
#[tokio::test]
async fn resolve_query_parses_nud() {
let query = resolve_query("reachable").await.expect("resolve query");
assert_eq!(query.filter.nuds, vec![NeighborState::Reachable]);
assert_eq!(query, vec![Query::NeighborState(NeighborState::Reachable)]);
}
#[tokio::test]
@@ -80,7 +83,7 @@ mod tests {
},
nud_state: None,
}];
let devices = merge_devices(neighbors, leases, &wakey_core::DeviceQuery::default());
let devices = merge_devices(neighbors, leases, &wakey_core::InventoryQuery::default());
assert_eq!(devices.len(), 1);
assert_eq!(devices[0].presence, Presence::Online);
assert_eq!(devices[0].names, vec!["pc".to_string()]);
+87 -18
View File
@@ -1,6 +1,7 @@
use anyhow::Result;
use wakey_core::{
Device, DeviceInventory, DeviceQuery, DhcpLease, DhcpLeaseWithState, NeighborEntry, Presence,
Device, DeviceInventory, DhcpLease, DhcpLeaseWithState, InventoryQuery, NeighborEntry,
Presence, Query,
};
use crate::service::leases::get_leases;
@@ -17,7 +18,7 @@ pub async fn resolve_devices(input: impl Into<String>) -> Result<Vec<Device>> {
/// This is the current center of gravity for the service layer. Higher-level
/// status and wake flows should prefer deriving from inventory rather than
/// directly from raw Linux source rows.
pub async fn inventory(query: DeviceQuery) -> Result<DeviceInventory> {
pub async fn inventory(query: InventoryQuery) -> Result<DeviceInventory> {
let neighbors = wakey_linux::devices::query_neighbors(&query).await?;
let leases = get_leases(wakey_core::LeaseQuery {
include_state: false,
@@ -35,7 +36,7 @@ pub async fn inventory(query: DeviceQuery) -> Result<DeviceInventory> {
pub fn merge_devices(
neighbors: Vec<NeighborEntry>,
leases: Vec<DhcpLeaseWithState>,
query: &DeviceQuery,
query: &InventoryQuery,
) -> Vec<Device> {
use std::collections::BTreeMap;
@@ -58,34 +59,45 @@ pub fn merge_devices(
.map(|(neighbors, leases)| Device::from_parts(neighbors, leases))
.collect();
if let Some(name) = &query.name {
devices.retain(|device| device.names.iter().any(|n| n == name));
let mut texts: Vec<&str> = Vec::new();
let mut devs: Vec<&str> = Vec::new();
let mut ips = Vec::new();
let mut macs = Vec::new();
let mut nuds = Vec::new();
for term in query {
match term {
Query::Text(v) => texts.push(v.as_str()),
Query::Interface(v) => devs.push(v.as_str()),
Query::Ip(v) => ips.push(*v),
Query::Mac(v) => macs.push(*v),
Query::NeighborState(v) => nuds.push(*v),
}
}
if !query.filter.devs.is_empty() {
if !texts.is_empty() {
devices.retain(|device| device.names.iter().any(|n| texts.iter().any(|t| n == t)));
}
if !devs.is_empty() {
devices.retain(|device| {
device
.interfaces
.iter()
.any(|iface| query.filter.devs.contains(iface))
.any(|iface| devs.iter().any(|d| iface == d))
});
}
if !query.filter.ips.is_empty() {
devices.retain(|device| device.ips.iter().any(|ip| query.filter.ips.contains(ip)));
if !ips.is_empty() {
devices.retain(|device| device.ips.iter().any(|ip| ips.contains(ip)));
}
if !query.filter.macs.is_empty() {
devices.retain(|device| {
device
.macs
.iter()
.any(|mac| query.filter.macs.contains(mac))
});
if !macs.is_empty() {
devices.retain(|device| device.macs.iter().any(|mac| macs.contains(mac)));
}
if !query.filter.nuds.is_empty() {
if !nuds.is_empty() {
devices.retain(|device| {
device
.neighbors
.iter()
.any(|neighbor| query.filter.nuds.contains(&neighbor.state))
.any(|neighbor| nuds.contains(&neighbor.state))
});
}
@@ -105,3 +117,60 @@ const fn presence_rank(presence: Presence) -> u8 {
Presence::Offline => 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{IpAddr, Ipv4Addr};
use wakey_core::{DhcpLease, InventoryQueryBuilder, NeighborState};
fn sample_neighbors() -> Vec<NeighborEntry> {
vec![NeighborEntry {
ip: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)),
dev: Some("br-lan".to_string()),
mac: Some("aa:bb:cc:dd:ee:ff".parse().expect("mac")),
state: NeighborState::Reachable,
}]
}
fn sample_leases() -> Vec<DhcpLeaseWithState> {
vec![DhcpLeaseWithState {
lease_line: DhcpLease {
expires_epoch: 1,
ip: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)),
mac: "aa:bb:cc:dd:ee:ff".parse().expect("mac"),
name: Some("pc".to_string()),
},
nud_state: None,
}]
}
#[test]
fn merge_devices_applies_and_across_categories() {
let query = InventoryQueryBuilder::new()
.maybe_text(Some("pc".to_string()))
.interfaces(vec!["br-lan".to_string()])
.neighbor_states(vec![NeighborState::Reachable])
.build();
let out = merge_devices(sample_neighbors(), sample_leases(), &query);
assert_eq!(out.len(), 1);
let no_match_query = InventoryQueryBuilder::new()
.maybe_text(Some("pc".to_string()))
.interfaces(vec!["eth9".to_string()])
.build();
let out = merge_devices(sample_neighbors(), sample_leases(), &no_match_query);
assert!(out.is_empty());
}
#[test]
fn merge_devices_allows_or_within_same_category() {
let query = InventoryQueryBuilder::new()
.neighbor_states(vec![NeighborState::Stale, NeighborState::Reachable])
.build();
let out = merge_devices(sample_neighbors(), sample_leases(), &query);
assert_eq!(out.len(), 1);
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ pub mod wake;
pub use interfaces::{get_interface_summaries, get_interface_summary, get_ips};
pub use inventory::{inventory, merge_devices, resolve_devices};
pub use leases::{get_leases, leases_without_state};
pub use query::{query_to_device_query, resolve_query, resolve_selector};
pub use query::{query_to_inventory_query, resolve_query, resolve_selector};
pub use wake::{
broadcast_wake_targets, resolve_wake_targets, wake_explicit, wake_from_query, wake_targets,
};
+8 -41
View File
@@ -1,12 +1,12 @@
use anyhow::Result;
use wakey_core::{DeviceFilters, DeviceQuery, Query, QueryInput};
use wakey_core::{InventoryQuery, Query, QueryInput};
/// Resolve free-form user input into the legacy `DeviceQuery` filter shape.
/// Resolve free-form user input into an `InventoryQuery` filter shape.
///
/// This is the compatibility entrypoint used by CLI and HTTP paths that still
/// speak in terms of `DeviceQuery`.
pub async fn resolve_query(input: impl Into<String>) -> Result<DeviceQuery> {
query_to_device_query(resolve_selector(input).await?)
/// speak in terms of query/filter payloads.
pub async fn resolve_query(input: impl Into<String>) -> Result<InventoryQuery> {
query_to_inventory_query(resolve_selector(input).await?)
}
/// Classify one piece of free-form user input into a typed selector.
@@ -25,43 +25,10 @@ pub async fn resolve_selector(input: impl Into<String>) -> Result<Query> {
)
}
/// Convert the newer selector-oriented `Query` model into a `DeviceQuery`.
/// Convert the newer selector-oriented `Query` model into an `InventoryQuery`.
///
/// This keeps the old filter-based service and HTTP surfaces working while the
/// internals migrate toward selector- and device-oriented APIs.
pub fn query_to_device_query(query: Query) -> Result<DeviceQuery> {
Ok(match query {
Query::Ip(ip_addr) => DeviceQuery {
filter: DeviceFilters {
ips: vec![ip_addr],
..Default::default()
},
..Default::default()
},
Query::Mac(mac_addr) => DeviceQuery {
filter: DeviceFilters {
macs: vec![mac_addr],
..Default::default()
},
..Default::default()
},
Query::Interface(dev) => DeviceQuery {
filter: DeviceFilters {
devs: vec![dev],
..Default::default()
},
..Default::default()
},
Query::NeighborState(state) => DeviceQuery {
filter: DeviceFilters {
nuds: vec![state],
..Default::default()
},
..Default::default()
},
Query::Text(name) => DeviceQuery {
name: Some(name),
..Default::default()
},
})
pub fn query_to_inventory_query(query: Query) -> Result<InventoryQuery> {
Ok(vec![query])
}