[WIP] A simple wake-on-lan service

improve config type

- all structs have default_property for non-struct properties
- all structs have default impls
- all properties use #[serde(default = "...")] to allow individual control
- links can now be defined as a toml `text = url` object
note: [text, url][] is also supported (and is used internally) incase duplicate text is needed

vielle.dev c595d953 885bdd75

verified
+173 -73
+130 -63
src/config.rs
··· 10 10 11 11 #[derive(Deserialize, Serialize, Debug, Clone)] 12 12 pub struct Config { 13 - #[serde(default = "default_binding")] 14 - binding: String, 15 - #[serde(default = "default_theme")] 16 - theme: Theme, 17 - #[serde(default = "default_info")] 18 - info: Info, 19 - pinned: Option<Vec<String>>, 20 - targets: HashMap<String, Target>, 13 + #[serde(default = "Config::default_binding")] 14 + pub binding: String, 15 + #[serde(default = "Config::default_pinned")] 16 + pub pinned: Vec<String>, 17 + #[serde(default = "Config::default_targets")] 18 + pub targets: HashMap<String, Target>, 19 + #[serde(default)] 20 + pub info: Info, 21 + #[serde(default)] 22 + pub theme: Theme, 21 23 } 22 24 23 25 /// all colours are in rgb 24 26 #[derive(Deserialize, Serialize, Debug, Clone)] 25 27 pub struct Theme { 26 - background: (u8, u8, u8), 27 - foreground: (u8, u8, u8), 28 - text: (u8, u8, u8), 29 - text_secondary: (u8, u8, u8), 30 - accent_success: (u8, u8, u8), 31 - accent_fail: (u8, u8, u8), 32 - link: (u8, u8, u8), 33 - link_visited: (u8, u8, u8), 34 - highlight: (u8, u8, u8), 35 - highlight_opacity: u8, 28 + #[serde(default = "Theme::default_background")] 29 + pub background: (u8, u8, u8), 30 + #[serde(default = "Theme::default_foreground")] 31 + pub foreground: (u8, u8, u8), 32 + #[serde(default = "Theme::default_text")] 33 + pub text: (u8, u8, u8), 34 + #[serde(default = "Theme::default_text_secondary")] 35 + pub text_secondary: (u8, u8, u8), 36 + #[serde(default = "Theme::default_accent_success")] 37 + pub accent_success: (u8, u8, u8), 38 + #[serde(default = "Theme::default_accent_fail")] 39 + pub accent_fail: (u8, u8, u8), 40 + #[serde(default = "Theme::default_link")] 41 + pub link: (u8, u8, u8), 42 + #[serde(default = "Theme::default_link_visited")] 43 + pub link_visited: (u8, u8, u8), 44 + #[serde(default = "Theme::default_highlight")] 45 + pub highlight: (u8, u8, u8), 46 + #[serde(default = "Theme::default_highlight_opacity")] 47 + pub highlight_opacity: u8, 36 48 } 37 49 38 50 #[derive(Deserialize, Serialize, Debug, Clone)] ··· 46 58 47 59 #[derive(Deserialize, Serialize, Debug, Clone)] 48 60 pub struct Info { 49 - title: String, 50 - links: Vec<(String, String)>, 51 - icon: Option<PathBuf>, 61 + #[serde(default = "Info::default_title")] 62 + pub title: String, 63 + #[serde(default = "Info::default_links", with = "crate::utils::links")] 64 + pub links: Vec<(String, String)>, 65 + #[serde(default = "Info::default_icon")] 66 + pub icon: PathBuf, 52 67 } 53 68 54 - fn default_binding() -> String { 55 - String::from("0.0.0.0:3000") 69 + impl Config { 70 + fn default_binding() -> String { 71 + String::from("0.0.0.0:3000") 72 + } 73 + fn default_pinned() -> Vec<String> { 74 + Vec::new() 75 + } 76 + fn default_targets() -> HashMap<String, Target> { 77 + HashMap::new() 78 + } 56 79 } 57 80 58 - fn default_theme() -> Theme { 59 - Theme { 60 - background: (48, 52, 70), 61 - foreground: (35, 38, 52), 62 - text: (198, 208, 245), 63 - text_secondary: (165, 173, 206), 64 - accent_success: (166, 209, 137), 65 - accent_fail: (231, 130, 132), 66 - link: (140, 170, 238), 67 - link_visited: (202, 158, 230), 68 - highlight: (148, 156, 187), 69 - highlight_opacity: 64, 81 + impl Default for Config { 82 + fn default() -> Self { 83 + Self { 84 + binding: Config::default_binding(), 85 + theme: Theme::default(), 86 + info: Info::default(), 87 + pinned: Config::default_pinned(), 88 + targets: Config::default_targets(), 89 + } 70 90 } 71 91 } 72 92 73 - fn default_info() -> Info { 74 - Info { 75 - title: "WOL".into(), 76 - links: vec![ 93 + impl Info { 94 + fn default_title() -> String { 95 + String::from("Wake on Lan") 96 + } 97 + 98 + fn default_links() -> Vec<(String, String)> { 99 + vec![ 77 100 ( 78 101 String::from("https://tangled.org/vielle.dev/wol/"), 79 102 String::from("vielle.dev/wol"), ··· 82 105 String::from("https://tangled.org/vielle.dev/wol/tree/main/docs/README.md"), 83 106 String::from("docs"), 84 107 ), 85 - ], 86 - icon: None, 108 + ] 109 + } 110 + 111 + fn default_icon() -> PathBuf { 112 + PathBuf::from("./favicon.ico") 113 + } 114 + } 115 + 116 + impl Default for Info { 117 + fn default() -> Self { 118 + Self { 119 + title: Info::default_title(), 120 + links: Info::default_links(), 121 + icon: Info::default_icon(), 122 + } 123 + } 124 + } 125 + 126 + impl Theme { 127 + fn default_background() -> (u8, u8, u8) { 128 + (48, 52, 70) 129 + } 130 + fn default_foreground() -> (u8, u8, u8) { 131 + (35, 38, 52) 132 + } 133 + fn default_text() -> (u8, u8, u8) { 134 + (198, 208, 245) 135 + } 136 + fn default_text_secondary() -> (u8, u8, u8) { 137 + (165, 173, 206) 138 + } 139 + fn default_accent_success() -> (u8, u8, u8) { 140 + (166, 209, 137) 141 + } 142 + fn default_accent_fail() -> (u8, u8, u8) { 143 + (231, 130, 132) 144 + } 145 + fn default_link() -> (u8, u8, u8) { 146 + (140, 170, 238) 147 + } 148 + fn default_link_visited() -> (u8, u8, u8) { 149 + (202, 158, 230) 150 + } 151 + fn default_highlight() -> (u8, u8, u8) { 152 + (148, 156, 187) 153 + } 154 + fn default_highlight_opacity() -> u8 { 155 + 25 156 + } 157 + } 158 + 159 + impl Default for Theme { 160 + fn default() -> Self { 161 + Self { 162 + background: Theme::default_background(), 163 + foreground: Theme::default_foreground(), 164 + text: Theme::default_text(), 165 + text_secondary: Theme::default_text_secondary(), 166 + accent_success: Theme::default_accent_success(), 167 + accent_fail: Theme::default_accent_fail(), 168 + link: Theme::default_link(), 169 + link_visited: Theme::default_link_visited(), 170 + highlight: Theme::default_highlight(), 171 + highlight_opacity: Theme::default_highlight_opacity(), 172 + } 87 173 } 88 174 } 89 175 ··· 108 194 109 195 // all entries in pinned should be keys of targets 110 196 let targets = config.targets.keys().collect::<Vec<_>>(); 111 - if let Some(mismatch) = &config 112 - .pinned 113 - .as_ref() 114 - .and_then(|p| p.iter().find(|x| !targets.contains(x))) 115 - { 197 + if let Some(mismatch) = &config.pinned.iter().find(|x| !targets.contains(x)) { 116 198 return Err(ConfigError::UnknownPin(mismatch.to_string())); 117 199 }; 118 200 119 201 let mut uniq = HashSet::<String>::new(); 120 202 if let Some(dupe) = &config 121 203 .pinned 122 - .as_ref() 123 - .and_then(|p| p.iter().find(move |x| !uniq.insert(x.to_string()))) 204 + .iter() 205 + .find(move |x| !uniq.insert(x.to_string())) 124 206 { 125 207 return Err(ConfigError::DupePin(dupe.to_string())); 126 208 }; 127 209 128 210 Ok(config) 129 - } 130 - 131 - #[allow(unused)] 132 - pub fn get_binding(&self) -> &String { 133 - &self.binding 134 - } 135 - 136 - #[allow(unused)] 137 - pub fn get_pinned(&self) -> &Option<Vec<String>> { 138 - &self.pinned 139 - } 140 - 141 - #[allow(unused)] 142 - pub fn get_targets(&self) -> &HashMap<String, Target> { 143 - &self.targets 144 211 } 145 212 }
+3 -3
src/main.rs
··· 19 19 async fn main() -> () { 20 20 async fn main() -> Result<(), Error> { 21 21 let config = config::Config::load(PathBuf::from("./wol.toml"))?; 22 - println!("Binding to {}", config.get_binding()); 23 - for (k, v) in config.get_targets() { 22 + println!("Binding to {}", config.binding); 23 + for (k, v) in &config.targets { 24 24 println!("target: {k}: {} ({:?})", v.mac, v.ip); 25 25 } 26 - let listener = tokio::net::TcpListener::bind(config.get_binding()).await?; 26 + let listener = tokio::net::TcpListener::bind(&config.binding).await?; 27 27 axum::serve(listener, server::router(config)).await?; 28 28 29 29 Ok(())
+34
src/utils.rs
··· 62 62 se.serialize_str(&mac.to_string()) 63 63 } 64 64 } 65 + 66 + pub mod links { 67 + use std::collections::HashMap; 68 + 69 + use serde::{Deserialize, Deserializer, Serializer, ser::SerializeSeq}; 70 + 71 + #[derive(Deserialize)] 72 + #[serde(untagged)] 73 + enum Inner { 74 + Vec(Vec<(String, String)>), 75 + Hash(HashMap<String, String>), 76 + } 77 + 78 + pub fn deserialize<'de, D>(de: D) -> Result<Vec<(String, String)>, D::Error> 79 + where 80 + D: Deserializer<'de>, 81 + { 82 + Ok(match Inner::deserialize(de)? { 83 + Inner::Vec(vec) => vec, 84 + Inner::Hash(hash) => hash.into_iter().collect(), 85 + }) 86 + } 87 + 88 + pub fn serialize<S>(links: &Vec<(String, String)>, se: S) -> Result<S::Ok, S::Error> 89 + where 90 + S: Serializer, 91 + { 92 + let mut seq = se.serialize_seq(Some(links.len()))?; 93 + for link in links { 94 + seq.serialize_element(link)?; 95 + } 96 + seq.end() 97 + } 98 + }
+4 -5
web/src/App.svelte
··· 5 5 6 6 const targets = [ 7 7 ...Object.entries(config.targets) 8 - .filter(([k, v]) => (config.pinned ?? []).includes(k)) 8 + .filter(([k, v]) => config.pinned.includes(k)) 9 9 .sort( 10 - ([k1], [k2]) => 11 - (config.pinned ?? []).indexOf(k1) - (config.pinned ?? []).indexOf(k2), 10 + ([k1], [k2]) => config.pinned.indexOf(k1) - config.pinned.indexOf(k2), 12 11 ), 13 12 ...Object.entries(config.targets).filter( 14 - ([k]) => !(config.pinned ?? []).includes(k), 13 + ([k]) => !config.pinned.includes(k), 15 14 ), 16 15 ]; 17 16 ··· 30 29 {/each} 31 30 </ul> 32 31 <ul class="links"> 33 - {#each config.info.links as [href, text]} 32 + {#each config.info.links as [text, href]} 34 33 <li><a {href}>{text}</a></li> 35 34 {/each} 36 35 </ul>
+2 -2
web/src/lib/api.ts
··· 81 81 info: z.object({ 82 82 title: z.string(), 83 83 links: z.array(z.tuple([z.string(), z.string()])), 84 - icon: z.string().nullable(), 84 + icon: z.string(), 85 85 }), 86 - pinned: z.array(z.string()).nullable(), 86 + pinned: z.array(z.string()), 87 87 targets: z.record( 88 88 z.string(), 89 89 z.object({