From 0765d94a5e9d64d49342b48f3cf3e49dc7a4d261 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Wed, 16 Aug 2023 00:35:11 -0400 Subject: [PATCH] Create a type-safe configuration library --- Cargo.lock | 67 +++-------------- Cargo.toml | 1 + build.sh | 1 + config-derive/Cargo.toml | 14 ++++ config-derive/src/lib.rs | 23 ++++++ config/Cargo.toml | 16 ++++ config/src/lib.rs | 154 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 218 insertions(+), 58 deletions(-) create mode 100644 config-derive/Cargo.toml create mode 100644 config-derive/src/lib.rs create mode 100644 config/Cargo.toml create mode 100644 config/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 397a0bf..533559f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,15 +74,6 @@ version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" -[[package]] -name = "basic-toml" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bfc506e7a2370ec239e1d072507b2a80c833083699d3c6fa176fbb4de8448c6" -dependencies = [ - "serde", -] - [[package]] name = "bit-set" version = "0.5.3" @@ -246,13 +237,19 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" name = "config" version = "0.1.0" dependencies = [ + "config-derive", "cool_asserts", - "quote", "serde", "serde_json", - "syn 1.0.109", "thiserror", - "trybuild", +] + +[[package]] +name = "config-derive" +version = "0.1.0" +dependencies = [ + "quote", + "syn 1.0.109", ] [[package]] @@ -407,12 +404,6 @@ dependencies = [ "syn 2.0.28", ] -[[package]] -name = "dissimilar" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86e3bdc80eee6e16b2b6b0f87fbc98c04bee3455e35174c0de1a125d0688c632" - [[package]] name = "either" version = "1.9.0" @@ -903,12 +894,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - [[package]] name = "gobject-sys" version = "0.17.10" @@ -2257,15 +2242,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "termcolor" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" -dependencies = [ - "winapi-util", -] - [[package]] name = "thiserror" version = "1.0.46" @@ -2452,22 +2428,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" -[[package]] -name = "trybuild" -version = "1.0.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a84e0202ea606ba5ebee8507ab2bfbe89b98551ed9b8f0be198109275cff284b" -dependencies = [ - "basic-toml", - "dissimilar", - "glob", - "once_cell", - "serde", - "serde_derive", - "serde_json", - "termcolor", -] - [[package]] name = "type-map" version = "0.4.0" @@ -2726,15 +2686,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 904e347..fcd6eff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "changeset", "config", + "config-derive", "coordinates", "cyberpunk-splash", "dashboard", diff --git a/build.sh b/build.sh index fdd2f32..579cc62 100755 --- a/build.sh +++ b/build.sh @@ -6,6 +6,7 @@ set -x RUST_ALL_TARGETS=( "changeset" "config" + "config-derive" "coordinates" "cyberpunk-splash" "dashboard" diff --git a/config-derive/Cargo.toml b/config-derive/Cargo.toml new file mode 100644 index 0000000..92aeba0 --- /dev/null +++ b/config-derive/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "config-derive" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +proc-macro = true + +[dependencies] +quote = { version = "1" } +syn = { version = "1", features = [ "extra-traits" ] } + diff --git a/config-derive/src/lib.rs b/config-derive/src/lib.rs new file mode 100644 index 0000000..9b9822d --- /dev/null +++ b/config-derive/src/lib.rs @@ -0,0 +1,23 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use quote::quote; + +use syn::{parse_macro_input, DeriveInput}; + +#[proc_macro_derive(ConfigOption)] +pub fn derive(input: TokenStream) -> TokenStream { + let DeriveInput { ident, .. } = parse_macro_input!(input as DeriveInput); + + let result = quote! { + impl From<&Config> for Option<#ident> { + fn from(config: &Config) -> Self { + match config.values.get(&ConfigName::#ident) { + Some(ConfigOption::#ident(val)) => Some(val.clone()), + _ => None, + } + } + } + }; + result.into() +} diff --git a/config/Cargo.toml b/config/Cargo.toml new file mode 100644 index 0000000..52c926b --- /dev/null +++ b/config/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "config" +version = "0.1.0" +edition = "2021" + + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +config-derive = { path = "../config-derive" } +serde_json = { version = "1" } +serde = { version = "1", features = [ "derive" ] } +thiserror = { version = "1" } + +[dev-dependencies] +cool_asserts = { version = "2" } diff --git a/config/src/lib.rs b/config/src/lib.rs new file mode 100644 index 0000000..ca80e9a --- /dev/null +++ b/config/src/lib.rs @@ -0,0 +1,154 @@ +pub use config_derive::ConfigOption; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ConfigReadError { + #[error("Cannot read the configuration file: {0}")] + CannotRead(std::io::Error), + #[error("Cannot open the configuration file for reading: {0}")] + CannotOpen(std::io::Error), + #[error("Invalid json data found in the configurationfile: {0}")] + InvalidJSON(serde_json::Error), +} + +macro_rules! define_config { + ($($name:ident($struct:ident),)+) => ( + use serde::{Deserialize, Serialize}; + use std::{ + collections::HashMap, + fs::File, + hash::Hash, + io::{ErrorKind, Read}, + path::PathBuf, + }; + + #[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] + pub enum ConfigName { + $($name),+ + } + + #[derive(Clone, Debug, Serialize, Deserialize)] + pub enum ConfigOption { + $($name($struct)),+ + } + + #[derive(Clone, Debug)] + pub struct Config { + values: HashMap, + } + + impl Config { + pub fn new() -> Self { + Self { + values: HashMap::new(), + } + } + + pub fn from_path(config_path: PathBuf) -> Result { + let mut settings = config_path.clone(); + settings.push("config"); + + match File::open(settings) { + Ok(mut file) => { + let mut buf = String::new(); + file.read_to_string(&mut buf) + .map_err(|err| ConfigReadError::CannotRead(err))?; + let values = serde_json::from_str(buf.as_ref()) + .map_err(|err| ConfigReadError::InvalidJSON(err))?; + Ok(Self { + values, + }) + } + Err(io_err) => { + match io_err.kind() { + ErrorKind::NotFound => { + /* create the path and an empty file */ + Ok(Self { + values: HashMap::new(), + }) + } + _ => Err(ConfigReadError::CannotOpen(io_err)), + } + } + } + } + + pub fn set(&mut self, val: ConfigOption) { + let _ = match val { + $(ConfigOption::$struct(_) => self.values.insert(ConfigName::$name, val)),+ + }; + } + + pub fn get<'a, T>(&'a self) -> Option + where + Option: From<&'a Self>, + { + self.into() + } + } + ) +} + +#[cfg(test)] +mod test { + use super::*; + use cool_asserts::assert_matches; + + define_config! { + DatabasePath(DatabasePath), + Me(Me), + } + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, ConfigOption)] + pub struct DatabasePath(PathBuf); + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + enum Rank { + Kyu(i8), + Dan(i8), + } + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, ConfigOption)] + pub struct Me { + name: String, + rank: Option, + } + + #[test] + fn it_can_set_and_get_options() { + let mut config: Config = Config::new(); + config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from( + "./fixtures/five_games", + )))); + + assert_eq!( + Some(DatabasePath(PathBuf::from("./fixtures/five_games"))), + config.get() + ); + } + + #[test] + fn it_can_serialize_and_deserialize() { + let mut config = Config::new(); + config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from( + "fixtures/five_games", + )))); + config.set(ConfigOption::Me(Me { + name: "Savanni".to_owned(), + rank: Some(Rank::Kyu(10)), + })); + let s = serde_json::to_string(&config.values).unwrap(); + println!("{}", s); + let values: HashMap = serde_json::from_str(s.as_ref()).unwrap(); + println!("options: {:?}", values); + + assert_matches!(values.get(&ConfigName::DatabasePath), + Some(ConfigOption::DatabasePath(ref db_path)) => + assert_eq!(Some(db_path.clone()), config.get()) + ); + + assert_matches!(values.get(&ConfigName::Me), Some(ConfigOption::Me(val)) => + assert_eq!(Some(val.clone()), config.get()) + ); + } +}