feat: basic
Some checks failed
CI / Rust Check (push) Has been cancelled
CI / Web UI Check (push) Has been cancelled
CI / Security Audit (push) Has been cancelled

This commit is contained in:
2025-07-12 23:59:42 +08:00
parent c7fe5373e1
commit dd11bc70b5
44 changed files with 9164 additions and 1 deletions

368
src/types.rs Normal file
View 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]);
}
}