From a4efc2e7281e0d979cefac5dd85f536b8a1cc14d Mon Sep 17 00:00:00 2001 From: Rain336 Date: Wed, 8 Nov 2023 01:52:03 +0100 Subject: [PATCH 1/3] First draft config macro --- Cargo.lock | 151 +++++++++++++++++++++++++++++ libs/config/Cargo.toml | 8 ++ libs/config/src/lib.rs | 3 + libs/config_macros/Cargo.toml | 13 +++ libs/config_macros/src/executer.rs | 123 +++++++++++++++++++++++ libs/config_macros/src/file.rs | 30 ++++++ libs/config_macros/src/lib.rs | 20 ++++ libs/config_macros/src/query.rs | 25 +++++ 8 files changed, 373 insertions(+) create mode 100644 libs/config/Cargo.toml create mode 100644 libs/config/src/lib.rs create mode 100644 libs/config_macros/Cargo.toml create mode 100644 libs/config_macros/src/executer.rs create mode 100644 libs/config_macros/src/file.rs create mode 100644 libs/config_macros/src/lib.rs create mode 100644 libs/config_macros/src/query.rs diff --git a/Cargo.lock b/Cargo.lock index f43d325..f0835a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,10 +42,48 @@ dependencies = [ "spinning_top", ] +[[package]] +name = "config" +version = "0.0.0" +dependencies = [ + "config_macros", +] + +[[package]] +name = "config_macros" +version = "0.0.0" +dependencies = [ + "quote", + "syn", + "toml", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "hashbrown" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" + [[package]] name = "hpet" version = "0.1.0" +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "interface" version = "0.0.0" @@ -95,6 +133,12 @@ dependencies = [ "uart_16550", ] +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + [[package]] name = "memory" version = "0.0.0" @@ -130,6 +174,24 @@ dependencies = [ "x86_64", ] +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + [[package]] name = "raw-cpuid" version = "10.6.0" @@ -172,6 +234,35 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "serde" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +dependencies = [ + "serde", +] + [[package]] name = "spinning_top" version = "0.3.0" @@ -181,6 +272,51 @@ dependencies = [ "lock_api", ] +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "uart_16550" version = "0.3.0" @@ -192,12 +328,27 @@ dependencies = [ "x86", ] +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + [[package]] name = "volatile" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "442887c63f2c839b346c192d047a7c87e73d0689c9157b00b53dcc27dd5ea793" +[[package]] +name = "winnow" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" +dependencies = [ + "memchr", +] + [[package]] name = "x86" version = "0.52.0" diff --git a/libs/config/Cargo.toml b/libs/config/Cargo.toml new file mode 100644 index 0000000..a5935fa --- /dev/null +++ b/libs/config/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "config" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +config_macros = { path = "../config_macros" } \ No newline at end of file diff --git a/libs/config/src/lib.rs b/libs/config/src/lib.rs new file mode 100644 index 0000000..b416449 --- /dev/null +++ b/libs/config/src/lib.rs @@ -0,0 +1,3 @@ +#![no_std] + +pub use config_macros::*; diff --git a/libs/config_macros/Cargo.toml b/libs/config_macros/Cargo.toml new file mode 100644 index 0000000..8b2c170 --- /dev/null +++ b/libs/config_macros/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "config_macros" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[lib] +proc-macro = true + +[dependencies] +syn = "2.0" +quote = "1.0" +toml = "0.8" diff --git a/libs/config_macros/src/executer.rs b/libs/config_macros/src/executer.rs new file mode 100644 index 0000000..d171add --- /dev/null +++ b/libs/config_macros/src/executer.rs @@ -0,0 +1,123 @@ +use crate::{file, ConfigQuery}; +use proc_macro::Span; +use syn::{Error, Expr, ExprLit, Lit, LitBool, LitFloat, LitInt, LitStr}; +use toml::value::Array; +use toml::{Table, Value}; + +#[derive(Clone, Copy)] +enum TableOrArray<'a> { + Table(&'a Table), + Array(&'a Array), +} + +impl<'a> TableOrArray<'a> { + fn get(self, segment: &str) -> syn::Result> { + Ok(match self { + TableOrArray::Table(x) => x.get(segment), + TableOrArray::Array(x) => x.get(segment.parse::().map_err(|err| { + Error::new( + Span::call_site().into(), + format!("Could not convert key segment '{segment}' into an Array index: {err}"), + ) + })?), + }) + } +} + +macro_rules! ret { + ($e:path, $ty:path, $val:expr) => { + return Ok(Expr::Lit(ExprLit { + attrs: Vec::new(), + lit: $e($ty($val, proc_macro::Span::call_site().into())), + })) + }; +} + +pub fn run(query: ConfigQuery) -> syn::Result { + let table = file::get_config()?; + let key = query.option.value(); + let parsed = parse_query(&key)?; + let last = parsed.len() - 1; + + let mut current = TableOrArray::Table(table); + for (idx, segment) in parsed.into_iter().enumerate() { + match current.get(segment)? { + Some(value) => match value { + Value::String(x) if idx == last => { + ret!(Lit::Str, LitStr::new, x) + } + Value::Integer(x) if idx == last => { + ret!(Lit::Int, LitInt::new, &x.to_string()) + } + Value::Float(x) if idx == last => { + ret!(Lit::Float, LitFloat::new, &x.to_string()) + } + Value::Boolean(x) if idx == last => { + ret!(Lit::Bool, LitBool::new, *x) + } + Value::Datetime(x) if idx == last => { + ret!(Lit::Str, LitStr::new, &x.to_string()) + } + Value::Array(x) => current = TableOrArray::Array(x), + Value::Table(x) => current = TableOrArray::Table(x), + _ => { + return Err(Error::new( + Span::call_site().into(), + format!("Key '{key}' hat at '{segment}' a non-Table / non-Array value"), + )) + } + }, + None => { + return match query.default { + Some(x) => Ok(x), + None => { + return Err(Error::new( + Span::call_site().into(), + format!("Key '{key}' has no value in the Config.toml"), + )) + } + } + } + } + } + + Err(Error::new( + Span::call_site().into(), + format!("Key '{key}' resolves to a table or Array"), + )) +} + +fn parse_query(mut query: &str) -> syn::Result> { + let mut result = Vec::new(); + + loop { + if query.starts_with(['"', '\'']) { + let idx = query[1..].find(['"', '\'']).ok_or_else(|| { + Error::new( + Span::call_site().into(), + "Quoted key is missing ending quote".to_string(), + ) + })?; + result.push(&query[1..idx - 1]); + + if !matches!(query[..idx + 1].bytes().next(), Some(b'.')) { + return Err(Error::new( + Span::call_site().into(), + "Expected '.' after quoted key".to_string(), + )); + } + + query = &query[..idx + 2]; + } else if let Some(idx) = query.find('.') { + result.push(&query[..idx]); + + query = &query[..idx + 1]; + } else { + result.push(query); + + break; + } + } + + Ok(result) +} diff --git a/libs/config_macros/src/file.rs b/libs/config_macros/src/file.rs new file mode 100644 index 0000000..5937733 --- /dev/null +++ b/libs/config_macros/src/file.rs @@ -0,0 +1,30 @@ +use proc_macro::Span; +use std::fs; +use std::sync::OnceLock; +use syn::Error; +use toml::Table; + +const CONFIG_TOML_PATH: &str = "Config.toml"; + +static CACHED_CONFIG: OnceLock = OnceLock::new(); + +pub fn get_config() -> syn::Result<&'static Table> { + if let Some(table) = CACHED_CONFIG.get() { + return Ok(table); + } + + let file = fs::read_to_string(CONFIG_TOML_PATH).map_err(|err| { + Error::new( + Span::call_site().into(), + format!("Could not read Config.toml: {err}"), + ) + })?; + let table = file.parse::
().map_err(|err| { + Error::new( + Span::call_site().into(), + format!("Couldn't parse Config.toml: {err}"), + ) + })?; + + Ok(CACHED_CONFIG.get_or_init(|| table)) +} diff --git a/libs/config_macros/src/lib.rs b/libs/config_macros/src/lib.rs new file mode 100644 index 0000000..339b11a --- /dev/null +++ b/libs/config_macros/src/lib.rs @@ -0,0 +1,20 @@ +use proc_macro::TokenStream; +use query::ConfigQuery; +use quote::ToTokens; +use syn::parse_macro_input; + +mod executer; +mod file; +mod query; + +#[proc_macro] +pub fn config(input: TokenStream) -> TokenStream { + let query = parse_macro_input!(input as ConfigQuery); + + let result = match executer::run(query) { + Ok(result) => result.into_token_stream(), + Err(err) => err.into_compile_error(), + }; + + result.into() +} diff --git a/libs/config_macros/src/query.rs b/libs/config_macros/src/query.rs new file mode 100644 index 0000000..eec9792 --- /dev/null +++ b/libs/config_macros/src/query.rs @@ -0,0 +1,25 @@ +use syn::parse::{Parse, ParseStream}; +use syn::{Expr, LitStr, Token}; + +pub struct ConfigQuery { + pub option: LitStr, + pub comma: Option, + pub default: Option, +} + +impl Parse for ConfigQuery { + fn parse(input: ParseStream) -> syn::Result { + let option = input.parse()?; + let (comma, default) = if let Ok(comma) = input.parse() { + (Some(comma), input.parse().ok()) + } else { + (None, None) + }; + + Ok(ConfigQuery { + option, + comma, + default, + }) + } +} From c23116f573d4b75634c8c7eef91d7dc17f9666e0 Mon Sep 17 00:00:00 2001 From: Rain336 Date: Fri, 10 Nov 2023 02:46:46 +0100 Subject: [PATCH 2/3] Added Unit Tests --- Cargo.lock | 1 + {libs => crates}/config/Cargo.toml | 0 {libs => crates}/config/src/lib.rs | 0 {libs => crates}/config_macros/Cargo.toml | 4 + crates/config_macros/src/executer.rs | 261 +++++++++++++++++++++ {libs => crates}/config_macros/src/file.rs | 13 +- {libs => crates}/config_macros/src/lib.rs | 0 crates/config_macros/src/query.rs | 66 ++++++ libs/config_macros/src/executer.rs | 123 ---------- libs/config_macros/src/query.rs | 25 -- 10 files changed, 342 insertions(+), 151 deletions(-) rename {libs => crates}/config/Cargo.toml (100%) rename {libs => crates}/config/src/lib.rs (100%) rename {libs => crates}/config_macros/Cargo.toml (66%) create mode 100644 crates/config_macros/src/executer.rs rename {libs => crates}/config_macros/src/file.rs (74%) rename {libs => crates}/config_macros/src/lib.rs (100%) create mode 100644 crates/config_macros/src/query.rs delete mode 100644 libs/config_macros/src/executer.rs delete mode 100644 libs/config_macros/src/query.rs diff --git a/Cargo.lock b/Cargo.lock index f0835a2..309bb6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -53,6 +53,7 @@ dependencies = [ name = "config_macros" version = "0.0.0" dependencies = [ + "proc-macro2", "quote", "syn", "toml", diff --git a/libs/config/Cargo.toml b/crates/config/Cargo.toml similarity index 100% rename from libs/config/Cargo.toml rename to crates/config/Cargo.toml diff --git a/libs/config/src/lib.rs b/crates/config/src/lib.rs similarity index 100% rename from libs/config/src/lib.rs rename to crates/config/src/lib.rs diff --git a/libs/config_macros/Cargo.toml b/crates/config_macros/Cargo.toml similarity index 66% rename from libs/config_macros/Cargo.toml rename to crates/config_macros/Cargo.toml index 8b2c170..b157383 100644 --- a/libs/config_macros/Cargo.toml +++ b/crates/config_macros/Cargo.toml @@ -11,3 +11,7 @@ proc-macro = true syn = "2.0" quote = "1.0" toml = "0.8" +proc-macro2 = "1.0" + +[dev-dependencies] +syn = { version = "2.0", features = ["extra-traits"] } diff --git a/crates/config_macros/src/executer.rs b/crates/config_macros/src/executer.rs new file mode 100644 index 0000000..45d3dd6 --- /dev/null +++ b/crates/config_macros/src/executer.rs @@ -0,0 +1,261 @@ +use crate::{file, ConfigQuery}; +use proc_macro2::Span; +use syn::{Error, Expr, ExprLit, LitBool, LitFloat, LitInt, LitStr}; +use toml::value::Array; +use toml::{Table, Value}; + +#[derive(Clone, Copy)] +enum TableOrArray<'a> { + Table(&'a Table), + Array(&'a Array), +} + +impl<'a> TableOrArray<'a> { + fn get(self, segment: &str) -> syn::Result> { + Ok(match self { + TableOrArray::Table(x) => x.get(segment), + TableOrArray::Array(x) => x.get(segment.parse::().map_err(|err| { + Error::new( + Span::call_site(), + format!("Could not convert key segment '{segment}' into an Array index: {err}"), + ) + })?), + }) + } +} + +macro_rules! ret { + ($ty:ident, $val:expr) => { + return Ok(Expr::Lit(ExprLit { + attrs: Vec::new(), + lit: ($ty::new($val, Span::call_site())).into(), + })) + }; +} + +pub fn run(query: ConfigQuery) -> syn::Result { + let table = file::get_config()?; + let key = query.option.value(); + let parsed = parse_query(&key)?; + let last = parsed.len() - 1; + + let mut current = TableOrArray::Table(table); + for (idx, segment) in parsed.into_iter().enumerate() { + match current.get(segment)? { + Some(value) => match value { + Value::String(x) if idx == last => { + ret!(LitStr, x) + } + Value::Integer(x) if idx == last => { + ret!(LitInt, &x.to_string()) + } + Value::Float(x) if idx == last => { + ret!(LitFloat, &x.to_string()) + } + Value::Boolean(x) if idx == last => { + ret!(LitBool, *x) + } + Value::Datetime(x) if idx == last => { + ret!(LitStr, &x.to_string()) + } + Value::Array(x) => current = TableOrArray::Array(x), + Value::Table(x) => current = TableOrArray::Table(x), + _ => { + return Err(Error::new( + Span::call_site(), + format!("Key '{key}' hat at '{segment}' a non-Table / non-Array value"), + )) + } + }, + None => { + return match query.default { + Some(x) => Ok(x), + None => { + return Err(Error::new( + Span::call_site(), + format!("Key '{key}' has no value in the Config.toml"), + )) + } + } + } + } + } + + Err(Error::new( + Span::call_site(), + format!("Key '{key}' resolves to a table or Array"), + )) +} + +fn parse_query(mut query: &str) -> syn::Result> { + let mut result = Vec::new(); + + while !query.is_empty() { + if query.starts_with('"') { + query = parse_until(&query[1..], &mut result, '"')?; + } else if query.starts_with('\'') { + query = parse_until(&query[1..], &mut result, '\'')?; + } else if let Some(idx) = query.find('.') { + result.push(&query[..idx]); + + query = &query[idx + 1..]; + } else { + result.push(query); + + break; + } + } + + Ok(result) +} + +fn parse_until<'a>( + mut query: &'a str, + result: &mut Vec<&'a str>, + end: char, +) -> syn::Result<&'a str> { + let idx = query.find(end).ok_or_else(|| { + Error::new( + Span::call_site(), + "Quoted key is missing ending quote".to_string(), + ) + })?; + result.push(&query[..idx]); + + query = &query[idx + 1..]; + + if !query.is_empty() { + if !matches!(query.bytes().next(), Some(b'.')) { + return Err(Error::new( + Span::call_site(), + "Expected '.' after quoted key".to_string(), + )); + } + + Ok(&query[1..]) + } else { + Ok(query) + } +} + +#[cfg(test)] +mod test { + use super::{parse_query, run}; + use crate::query::ConfigQuery; + use proc_macro2::Span; + use syn::{Expr, LitStr, LitInt, LitBool, LitFloat, ExprLit}; + + macro_rules! assert_run { + (default $input:expr, $default:expr) => { + let default = Expr::Lit(ExprLit { + attrs: Vec::new(), + lit: ($default).into(), + }); + let result = run(ConfigQuery { + option: LitStr::new($input, Span::call_site()), + comma: None, + default: Some(default.clone()), + }).unwrap(); + + assert_eq!(result, default); + }; + (fail $input:expr) => { + let result = run(ConfigQuery { + option: LitStr::new($input, Span::call_site()), + comma: None, + default: None, + }); + + if let Ok(result) = result { + assert!(false, "Expected run to fail, but got: {:?}", result); + } + }; + ($input:expr, $result:expr) => { + let result = run(ConfigQuery { + option: LitStr::new($input, Span::call_site()), + comma: None, + default: None, + }).unwrap(); + + let Expr::Lit(result) = result else { + assert!(false, "Resolved expr is not a literal."); + unreachable!() + }; + + assert_eq!(result.lit, ($result).into()); + }; + } + + #[test] + fn test_run() { + crate::file::set_config(toml::toml! { + foo = 5 + bar.baz = true + bar.string = "Hello World" + bar.float = 3.1415926535 + + [deeply.nested] + table.value = false + array = [1, 2, 3] + table.array = [ { foo = "wow" }, { foo = 4 }, { bar = 69420 } ] + }); + + assert_run!("foo", LitInt::new("5", Span::call_site())); + assert_run!("bar.baz", LitBool::new(true, Span::call_site())); + assert_run!("bar.string", LitStr::new("Hello World", Span::call_site())); + assert_run!("bar.float", LitFloat::new("3.1415926535", Span::call_site())); + assert_run!("deeply.nested.table.value", LitBool::new(false, Span::call_site())); + assert_run!("deeply.nested.array.1", LitInt::new("2", Span::call_site())); + assert_run!("deeply.nested.table.array.0.foo", LitStr::new("wow", Span::call_site())); + assert_run!("deeply.nested.table.array.2.bar", LitInt::new("69420", Span::call_site())); + + assert_run!(fail "bar"); + assert_run!(fail "deeply.nested"); + assert_run!(fail "deeply.nested.array"); + assert_run!(fail "deeply.nested.table.array"); + + assert_run!(default "missing", LitStr::new("Rawr", Span::call_site())); + assert_run!(default "missing.nested.key", LitStr::new("Yip", Span::call_site())); + } + + macro_rules! assert_query { + (fail $input:expr) => { + if let Ok(segments) = parse_query($input) { + assert!(false, "Expected parsing to fail, but got: {:?}", segments); + } + }; + ($input:expr, $( $output:expr ),* $(,)?) => { + let segments = parse_query($input).unwrap(); + assert_eq!(segments, vec![$( $output ),*]); + }; + } + + #[test] + fn test_parse_query() { + assert_query!("foo", "foo"); + assert_query!("foo.bar", "foo", "bar"); + assert_query!("foo.bar.'baz'", "foo", "bar", "baz"); + assert_query!("foo.bar.\"baz\"", "foo", "bar", "baz"); + assert_query!("foo.'bar'.\"baz\"", "foo", "bar", "baz"); + assert_query!("foo.'bar'.'baz'", "foo", "bar", "baz"); + assert_query!("foo.\"bar\".'baz'", "foo", "bar", "baz"); + assert_query!("'a very'.'complicated key'", "a very", "complicated key"); + assert_query!( + "'a very'.'complicated key'.'with a \" in it'", + "a very", + "complicated key", + "with a \" in it" + ); + assert_query!( + "'a very'.'complicated key'.\"with a ' in it\"", + "a very", + "complicated key", + "with a ' in it" + ); + + assert_query!(fail "'this is miss a quote"); + assert_query!(fail "\"this is miss a quote"); + assert_query!(fail "'this is also invalid''"); + assert_query!(fail "\"this is also invalid\"\""); + } +} diff --git a/libs/config_macros/src/file.rs b/crates/config_macros/src/file.rs similarity index 74% rename from libs/config_macros/src/file.rs rename to crates/config_macros/src/file.rs index 5937733..1ad2a34 100644 --- a/libs/config_macros/src/file.rs +++ b/crates/config_macros/src/file.rs @@ -1,4 +1,4 @@ -use proc_macro::Span; +use proc_macro2::Span; use std::fs; use std::sync::OnceLock; use syn::Error; @@ -15,16 +15,23 @@ pub fn get_config() -> syn::Result<&'static Table> { let file = fs::read_to_string(CONFIG_TOML_PATH).map_err(|err| { Error::new( - Span::call_site().into(), + Span::call_site(), format!("Could not read Config.toml: {err}"), ) })?; let table = file.parse::
().map_err(|err| { Error::new( - Span::call_site().into(), + Span::call_site(), format!("Couldn't parse Config.toml: {err}"), ) })?; Ok(CACHED_CONFIG.get_or_init(|| table)) } + +#[cfg(test)] +pub fn set_config(table: Table) { + CACHED_CONFIG + .set(table) + .expect("Config already set by another test."); +} diff --git a/libs/config_macros/src/lib.rs b/crates/config_macros/src/lib.rs similarity index 100% rename from libs/config_macros/src/lib.rs rename to crates/config_macros/src/lib.rs diff --git a/crates/config_macros/src/query.rs b/crates/config_macros/src/query.rs new file mode 100644 index 0000000..fed03a9 --- /dev/null +++ b/crates/config_macros/src/query.rs @@ -0,0 +1,66 @@ +use syn::parse::{Parse, ParseStream}; +use syn::{Expr, LitStr, Token}; + +pub struct ConfigQuery { + pub option: LitStr, + pub comma: Option, + pub default: Option, +} + +impl Parse for ConfigQuery { + fn parse(input: ParseStream) -> syn::Result { + let option = input.parse()?; + let (comma, default) = if let Ok(comma) = input.parse() { + (Some(comma), input.parse().ok()) + } else { + (None, None) + }; + + Ok(ConfigQuery { + option, + comma, + default, + }) + } +} + +#[cfg(test)] +mod test { + use super::ConfigQuery; + + macro_rules! assert_query { + ($input:expr, $option:expr, $comma:pat, $default:pat) => { + let stream = syn::parse_str::($input).unwrap(); + assert_eq!(stream.option.value(), $option); + assert!(matches!(stream.comma, $comma)); + assert!(matches!(stream.default, $default)); + }; + ($input:expr, $option:expr, $comma:pat) => { + let stream = syn::parse_str::($input).unwrap(); + assert_eq!(stream.option.value(), $option); + assert!(matches!(stream.comma, $comma)); + assert!(matches!(stream.default, None)); + }; + ($input:expr, $option:expr) => { + let stream = syn::parse_str::($input).unwrap(); + assert_eq!(stream.option.value(), $option); + assert!(matches!(stream.comma, None)); + assert!(matches!(stream.default, None)); + }; + } + + #[test] + fn test_parse() { + assert_query!("\"foo\"", "foo"); + + assert_query!("\"foo.bar\"", "foo.bar"); + + assert_query!("\"foo.'bar'\"", "foo.'bar'"); + + assert_query!("\"foo.\\\"bar\\\"\"", "foo.\"bar\""); + + assert_query!("\"foo.bar.baz\",", "foo.bar.baz", Some(_)); + + assert_query!("\"foo.'bar'\", true", "foo.'bar'", Some(_), Some(_)); + } +} diff --git a/libs/config_macros/src/executer.rs b/libs/config_macros/src/executer.rs deleted file mode 100644 index d171add..0000000 --- a/libs/config_macros/src/executer.rs +++ /dev/null @@ -1,123 +0,0 @@ -use crate::{file, ConfigQuery}; -use proc_macro::Span; -use syn::{Error, Expr, ExprLit, Lit, LitBool, LitFloat, LitInt, LitStr}; -use toml::value::Array; -use toml::{Table, Value}; - -#[derive(Clone, Copy)] -enum TableOrArray<'a> { - Table(&'a Table), - Array(&'a Array), -} - -impl<'a> TableOrArray<'a> { - fn get(self, segment: &str) -> syn::Result> { - Ok(match self { - TableOrArray::Table(x) => x.get(segment), - TableOrArray::Array(x) => x.get(segment.parse::().map_err(|err| { - Error::new( - Span::call_site().into(), - format!("Could not convert key segment '{segment}' into an Array index: {err}"), - ) - })?), - }) - } -} - -macro_rules! ret { - ($e:path, $ty:path, $val:expr) => { - return Ok(Expr::Lit(ExprLit { - attrs: Vec::new(), - lit: $e($ty($val, proc_macro::Span::call_site().into())), - })) - }; -} - -pub fn run(query: ConfigQuery) -> syn::Result { - let table = file::get_config()?; - let key = query.option.value(); - let parsed = parse_query(&key)?; - let last = parsed.len() - 1; - - let mut current = TableOrArray::Table(table); - for (idx, segment) in parsed.into_iter().enumerate() { - match current.get(segment)? { - Some(value) => match value { - Value::String(x) if idx == last => { - ret!(Lit::Str, LitStr::new, x) - } - Value::Integer(x) if idx == last => { - ret!(Lit::Int, LitInt::new, &x.to_string()) - } - Value::Float(x) if idx == last => { - ret!(Lit::Float, LitFloat::new, &x.to_string()) - } - Value::Boolean(x) if idx == last => { - ret!(Lit::Bool, LitBool::new, *x) - } - Value::Datetime(x) if idx == last => { - ret!(Lit::Str, LitStr::new, &x.to_string()) - } - Value::Array(x) => current = TableOrArray::Array(x), - Value::Table(x) => current = TableOrArray::Table(x), - _ => { - return Err(Error::new( - Span::call_site().into(), - format!("Key '{key}' hat at '{segment}' a non-Table / non-Array value"), - )) - } - }, - None => { - return match query.default { - Some(x) => Ok(x), - None => { - return Err(Error::new( - Span::call_site().into(), - format!("Key '{key}' has no value in the Config.toml"), - )) - } - } - } - } - } - - Err(Error::new( - Span::call_site().into(), - format!("Key '{key}' resolves to a table or Array"), - )) -} - -fn parse_query(mut query: &str) -> syn::Result> { - let mut result = Vec::new(); - - loop { - if query.starts_with(['"', '\'']) { - let idx = query[1..].find(['"', '\'']).ok_or_else(|| { - Error::new( - Span::call_site().into(), - "Quoted key is missing ending quote".to_string(), - ) - })?; - result.push(&query[1..idx - 1]); - - if !matches!(query[..idx + 1].bytes().next(), Some(b'.')) { - return Err(Error::new( - Span::call_site().into(), - "Expected '.' after quoted key".to_string(), - )); - } - - query = &query[..idx + 2]; - } else if let Some(idx) = query.find('.') { - result.push(&query[..idx]); - - query = &query[..idx + 1]; - } else { - result.push(query); - - break; - } - } - - Ok(result) -} diff --git a/libs/config_macros/src/query.rs b/libs/config_macros/src/query.rs deleted file mode 100644 index eec9792..0000000 --- a/libs/config_macros/src/query.rs +++ /dev/null @@ -1,25 +0,0 @@ -use syn::parse::{Parse, ParseStream}; -use syn::{Expr, LitStr, Token}; - -pub struct ConfigQuery { - pub option: LitStr, - pub comma: Option, - pub default: Option, -} - -impl Parse for ConfigQuery { - fn parse(input: ParseStream) -> syn::Result { - let option = input.parse()?; - let (comma, default) = if let Ok(comma) = input.parse() { - (Some(comma), input.parse().ok()) - } else { - (None, None) - }; - - Ok(ConfigQuery { - option, - comma, - default, - }) - } -} From 1653350f5a037f8c5325b924a6e43c41f39290ee Mon Sep 17 00:00:00 2001 From: Rain336 Date: Fri, 10 Nov 2023 02:51:54 +0100 Subject: [PATCH 3/3] Use workspace versions and added license headers --- Cargo.toml | 5 +++++ crates/config/src/lib.rs | 3 +++ crates/config_macros/Cargo.toml | 10 +++++----- crates/config_macros/src/executer.rs | 3 +++ crates/config_macros/src/file.rs | 3 +++ crates/config_macros/src/lib.rs | 3 +++ crates/config_macros/src/query.rs | 3 +++ 7 files changed, 25 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ab1a814..b13e3c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,8 @@ raw-cpuid = "11.0.1" uart_16550 = "0.3" noto-sans-mono-bitmap = "0.2" x86_64 = "0.14.11" + +syn = "2.0" +quote = "1.0" +toml = "0.8" +proc-macro2 = "1.0" diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index b416449..c05c6e4 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -1,3 +1,6 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. #![no_std] pub use config_macros::*; diff --git a/crates/config_macros/Cargo.toml b/crates/config_macros/Cargo.toml index b157383..5c24dd1 100644 --- a/crates/config_macros/Cargo.toml +++ b/crates/config_macros/Cargo.toml @@ -8,10 +8,10 @@ publish.workspace = true proc-macro = true [dependencies] -syn = "2.0" -quote = "1.0" -toml = "0.8" -proc-macro2 = "1.0" +syn = { workspace = true } +quote = { workspace = true } +toml = { workspace = true } +proc-macro2 = { workspace = true } [dev-dependencies] -syn = { version = "2.0", features = ["extra-traits"] } +syn = { workspace = true, features = ["extra-traits"] } diff --git a/crates/config_macros/src/executer.rs b/crates/config_macros/src/executer.rs index 45d3dd6..779e11d 100644 --- a/crates/config_macros/src/executer.rs +++ b/crates/config_macros/src/executer.rs @@ -1,3 +1,6 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. use crate::{file, ConfigQuery}; use proc_macro2::Span; use syn::{Error, Expr, ExprLit, LitBool, LitFloat, LitInt, LitStr}; diff --git a/crates/config_macros/src/file.rs b/crates/config_macros/src/file.rs index 1ad2a34..476bc27 100644 --- a/crates/config_macros/src/file.rs +++ b/crates/config_macros/src/file.rs @@ -1,3 +1,6 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. use proc_macro2::Span; use std::fs; use std::sync::OnceLock; diff --git a/crates/config_macros/src/lib.rs b/crates/config_macros/src/lib.rs index 339b11a..76008bb 100644 --- a/crates/config_macros/src/lib.rs +++ b/crates/config_macros/src/lib.rs @@ -1,3 +1,6 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. use proc_macro::TokenStream; use query::ConfigQuery; use quote::ToTokens; diff --git a/crates/config_macros/src/query.rs b/crates/config_macros/src/query.rs index fed03a9..c7735a0 100644 --- a/crates/config_macros/src/query.rs +++ b/crates/config_macros/src/query.rs @@ -1,3 +1,6 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. use syn::parse::{Parse, ParseStream}; use syn::{Expr, LitStr, Token};