diff --git a/Cargo.lock b/Cargo.lock index f43d325..309bb6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,10 +42,49 @@ dependencies = [ "spinning_top", ] +[[package]] +name = "config" +version = "0.0.0" +dependencies = [ + "config_macros", +] + +[[package]] +name = "config_macros" +version = "0.0.0" +dependencies = [ + "proc-macro2", + "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 +134,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 +175,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 +235,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 +273,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 +329,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/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/Cargo.toml b/crates/config/Cargo.toml new file mode 100644 index 0000000..a5935fa --- /dev/null +++ b/crates/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/crates/config/src/lib.rs b/crates/config/src/lib.rs new file mode 100644 index 0000000..c05c6e4 --- /dev/null +++ b/crates/config/src/lib.rs @@ -0,0 +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 new file mode 100644 index 0000000..5c24dd1 --- /dev/null +++ b/crates/config_macros/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "config_macros" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[lib] +proc-macro = true + +[dependencies] +syn = { workspace = true } +quote = { workspace = true } +toml = { workspace = true } +proc-macro2 = { workspace = true } + +[dev-dependencies] +syn = { workspace = true, 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..779e11d --- /dev/null +++ b/crates/config_macros/src/executer.rs @@ -0,0 +1,264 @@ +// 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}; +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/crates/config_macros/src/file.rs b/crates/config_macros/src/file.rs new file mode 100644 index 0000000..476bc27 --- /dev/null +++ b/crates/config_macros/src/file.rs @@ -0,0 +1,40 @@ +// 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; +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(), + format!("Could not read Config.toml: {err}"), + ) + })?; + let table = file.parse::
().map_err(|err| { + Error::new( + 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/crates/config_macros/src/lib.rs b/crates/config_macros/src/lib.rs new file mode 100644 index 0000000..76008bb --- /dev/null +++ b/crates/config_macros/src/lib.rs @@ -0,0 +1,23 @@ +// 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; +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/crates/config_macros/src/query.rs b/crates/config_macros/src/query.rs new file mode 100644 index 0000000..c7735a0 --- /dev/null +++ b/crates/config_macros/src/query.rs @@ -0,0 +1,69 @@ +// 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}; + +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(_)); + } +}