feat: basic
This commit is contained in:
368
src/types.rs
Normal file
368
src/types.rs
Normal file
@@ -0,0 +1,368 @@
|
||||
//! 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]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user