369 lines
9.5 KiB
Rust
369 lines
9.5 KiB
Rust
//! Core types for Galvanize
|
|
//!
|
|
//! This module defines the core data structures used throughout the application.
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::fmt;
|
|
use std::str::FromStr;
|
|
|
|
/// MAC address representation
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[serde(try_from = "String", into = "String")]
|
|
pub struct MacAddress([u8; 6]);
|
|
|
|
impl MacAddress {
|
|
/// Create a new MAC address from bytes
|
|
pub fn new(bytes: [u8; 6]) -> Self {
|
|
Self(bytes)
|
|
}
|
|
|
|
/// Get the raw bytes of the MAC address
|
|
pub fn as_bytes(&self) -> &[u8; 6] {
|
|
&self.0
|
|
}
|
|
|
|
/// Parse a MAC address from a string
|
|
pub fn parse(s: &str) -> Result<Self, MacAddressParseError> {
|
|
let s = s.trim();
|
|
|
|
// Try different separators: ':', '-', or none
|
|
let parts: Vec<&str> = if s.contains(':') {
|
|
s.split(':').collect()
|
|
} else if s.contains('-') {
|
|
s.split('-').collect()
|
|
} else if s.len() == 12 {
|
|
// No separator, split every 2 characters
|
|
(0..6).map(|i| &s[i * 2..i * 2 + 2]).collect()
|
|
} else {
|
|
return Err(MacAddressParseError::InvalidFormat);
|
|
};
|
|
|
|
if parts.len() != 6 {
|
|
return Err(MacAddressParseError::InvalidLength);
|
|
}
|
|
|
|
let mut bytes = [0u8; 6];
|
|
for (i, part) in parts.iter().enumerate() {
|
|
bytes[i] =
|
|
u8::from_str_radix(part, 16).map_err(|_| MacAddressParseError::InvalidHex)?;
|
|
}
|
|
|
|
Ok(Self(bytes))
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for MacAddress {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(
|
|
f,
|
|
"{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
|
|
self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5]
|
|
)
|
|
}
|
|
}
|
|
|
|
impl FromStr for MacAddress {
|
|
type Err = MacAddressParseError;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
Self::parse(s)
|
|
}
|
|
}
|
|
|
|
impl TryFrom<String> for MacAddress {
|
|
type Error = MacAddressParseError;
|
|
|
|
fn try_from(s: String) -> Result<Self, Self::Error> {
|
|
Self::parse(&s)
|
|
}
|
|
}
|
|
|
|
impl From<MacAddress> for String {
|
|
fn from(mac: MacAddress) -> Self {
|
|
mac.to_string()
|
|
}
|
|
}
|
|
|
|
/// Error parsing a MAC address
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum MacAddressParseError {
|
|
InvalidFormat,
|
|
InvalidLength,
|
|
InvalidHex,
|
|
}
|
|
|
|
impl fmt::Display for MacAddressParseError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::InvalidFormat => write!(f, "invalid MAC address format"),
|
|
Self::InvalidLength => write!(f, "invalid MAC address length"),
|
|
Self::InvalidHex => write!(f, "invalid hexadecimal in MAC address"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for MacAddressParseError {}
|
|
|
|
/// Device configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Device {
|
|
/// Unique identifier for the device
|
|
pub id: String,
|
|
|
|
/// Human-readable name
|
|
pub name: String,
|
|
|
|
/// Optional description
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub description: Option<String>,
|
|
|
|
/// MAC address of the device
|
|
pub mac: MacAddress,
|
|
|
|
/// Broadcast address for WoL packet (defaults to 255.255.255.255)
|
|
#[serde(default = "default_broadcast")]
|
|
pub broadcast: String,
|
|
|
|
/// Port for WoL packet (defaults to 9)
|
|
#[serde(default = "default_port")]
|
|
pub port: u16,
|
|
|
|
/// Network interface to use (optional)
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub interface: Option<String>,
|
|
|
|
/// Tags for organizing devices
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
pub tags: Vec<String>,
|
|
|
|
/// Custom metadata
|
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
|
pub metadata: HashMap<String, String>,
|
|
|
|
/// Whether the device is enabled
|
|
#[serde(default = "default_enabled")]
|
|
pub enabled: bool,
|
|
|
|
/// Creation timestamp
|
|
#[serde(default = "Utc::now")]
|
|
pub created_at: DateTime<Utc>,
|
|
|
|
/// Last update timestamp
|
|
#[serde(default = "Utc::now")]
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
fn default_broadcast() -> String {
|
|
"255.255.255.255".to_string()
|
|
}
|
|
|
|
fn default_port() -> u16 {
|
|
9
|
|
}
|
|
|
|
fn default_enabled() -> bool {
|
|
true
|
|
}
|
|
|
|
/// Request to create a new device
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CreateDeviceRequest {
|
|
/// Optional ID (generated if not provided)
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub id: Option<String>,
|
|
|
|
/// Human-readable name
|
|
pub name: String,
|
|
|
|
/// Optional description
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub description: Option<String>,
|
|
|
|
/// MAC address of the device
|
|
pub mac: String,
|
|
|
|
/// Broadcast address for WoL packet
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub broadcast: Option<String>,
|
|
|
|
/// Port for WoL packet
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub port: Option<u16>,
|
|
|
|
/// Network interface to use
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub interface: Option<String>,
|
|
|
|
/// Tags for organizing devices
|
|
#[serde(default)]
|
|
pub tags: Vec<String>,
|
|
|
|
/// Custom metadata
|
|
#[serde(default)]
|
|
pub metadata: HashMap<String, String>,
|
|
}
|
|
|
|
/// Request to update a device
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct UpdateDeviceRequest {
|
|
/// Human-readable name
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub name: Option<String>,
|
|
|
|
/// Optional description
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub description: Option<String>,
|
|
|
|
/// MAC address of the device
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub mac: Option<String>,
|
|
|
|
/// Broadcast address for WoL packet
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub broadcast: Option<String>,
|
|
|
|
/// Port for WoL packet
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub port: Option<u16>,
|
|
|
|
/// Network interface to use
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub interface: Option<String>,
|
|
|
|
/// Tags for organizing devices
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub tags: Option<Vec<String>>,
|
|
|
|
/// Custom metadata
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub metadata: Option<HashMap<String, String>>,
|
|
|
|
/// Whether the device is enabled
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub enabled: Option<bool>,
|
|
}
|
|
|
|
/// Request to wake a device by MAC address
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct WakeRequest {
|
|
/// MAC address of the device to wake
|
|
pub mac: String,
|
|
|
|
/// Optional broadcast address
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub broadcast: Option<String>,
|
|
|
|
/// Optional port
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub port: Option<u16>,
|
|
}
|
|
|
|
/// Response after waking a device
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct WakeResponse {
|
|
pub success: bool,
|
|
pub message: String,
|
|
pub device_id: Option<String>,
|
|
pub mac: String,
|
|
}
|
|
|
|
/// User information
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct User {
|
|
pub username: String,
|
|
#[serde(skip_serializing)]
|
|
pub password_hash: String,
|
|
pub roles: Vec<String>,
|
|
}
|
|
|
|
/// User role
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
#[allow(dead_code)]
|
|
pub enum Role {
|
|
Admin,
|
|
User,
|
|
Viewer,
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
impl Role {
|
|
pub fn can_wake(&self) -> bool {
|
|
matches!(self, Role::Admin | Role::User)
|
|
}
|
|
|
|
pub fn can_modify(&self) -> bool {
|
|
matches!(self, Role::Admin)
|
|
}
|
|
|
|
pub fn can_view(&self) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
impl FromStr for Role {
|
|
type Err = ();
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s.to_lowercase().as_str() {
|
|
"admin" => Ok(Role::Admin),
|
|
"user" => Ok(Role::User),
|
|
"viewer" => Ok(Role::Viewer),
|
|
_ => Err(()),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Health check response
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct HealthResponse {
|
|
pub status: String,
|
|
pub version: String,
|
|
pub uptime_seconds: u64,
|
|
}
|
|
|
|
/// List response wrapper
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ListResponse<T> {
|
|
pub items: Vec<T>,
|
|
pub total: usize,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_mac_address_parse_colon() {
|
|
let mac = MacAddress::parse("AA:BB:CC:DD:EE:FF").unwrap();
|
|
assert_eq!(mac.as_bytes(), &[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mac_address_parse_dash() {
|
|
let mac = MacAddress::parse("AA-BB-CC-DD-EE-FF").unwrap();
|
|
assert_eq!(mac.as_bytes(), &[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mac_address_parse_no_separator() {
|
|
let mac = MacAddress::parse("AABBCCDDEEFF").unwrap();
|
|
assert_eq!(mac.as_bytes(), &[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mac_address_display() {
|
|
let mac = MacAddress::new([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
|
|
assert_eq!(mac.to_string(), "AA:BB:CC:DD:EE:FF");
|
|
}
|
|
|
|
#[test]
|
|
fn test_mac_address_lowercase() {
|
|
let mac = MacAddress::parse("aa:bb:cc:dd:ee:ff").unwrap();
|
|
assert_eq!(mac.as_bytes(), &[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
|
|
}
|
|
}
|
|
|