diff --git a/Cargo.lock b/Cargo.lock index 3cb3bd0a..81167f6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,12 +3,14 @@ version = 4 [[package]] -name = "aho-corasick" -version = "0.7.20" +name = "ahash" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "memchr", + "getrandom 0.2.17", + "once_cell", + "version_check", ] [[package]] @@ -93,6 +95,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "async-channel" version = "1.9.0" @@ -296,6 +304,18 @@ dependencies = [ "typenum", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blocking" version = "1.6.2" @@ -309,12 +329,70 @@ dependencies = [ "piper", ] +[[package]] +name = "borsh" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "byte-unit" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d" +dependencies = [ + "rust_decimal", + "schemars", + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytes" version = "1.11.1" @@ -346,6 +424,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chacha20" version = "0.10.0" @@ -604,6 +688,12 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aeda16ab4059c5fd2a83f2b9c9e9c981327b18aa8e3b313f7e6563799d4f093e" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "dynamic-benches" version = "0.1.0" @@ -635,70 +725,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" -[[package]] -name = "encoding" -version = "0.2.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" -dependencies = [ - "encoding-index-japanese", - "encoding-index-korean", - "encoding-index-simpchinese", - "encoding-index-singlebyte", - "encoding-index-tradchinese", -] - -[[package]] -name = "encoding-index-japanese" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-korean" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-simpchinese" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-singlebyte" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-tradchinese" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding_index_tests" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" - [[package]] name = "enum-as-inner" version = "0.6.1" @@ -821,6 +847,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.32" @@ -988,6 +1020,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -1080,23 +1121,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "hocon" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dbf0338dac82c09762a1283d8bb117f2d0dddb97ee4eec711dd1b78f89975ee" -dependencies = [ - "aho-corasick 0.7.20", - "java-properties", - "lazy_static", - "linked-hash-map", - "memchr", - "nom", - "serde_path_to_error", - "thiserror", - "uuid", -] - [[package]] name = "home" version = "0.5.12" @@ -1106,6 +1130,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -1250,7 +1280,7 @@ dependencies = [ "rand_xoshiro", "sized-chunks", "typenum", - "version_check 0.9.5", + "version_check", ] [[package]] @@ -1319,17 +1349,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "java-properties" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1904d8654a1ef51034d02d5a9411b50bf91bea15b0ab644ae179d1325976263" -dependencies = [ - "encoding", - "lazy_static", - "regex", -] - [[package]] name = "js-sys" version = "0.3.95" @@ -1348,6 +1367,7 @@ version = "0.11.3" dependencies = [ "arc-swap", "async-std", + "byte-unit", "bytes", "core_affinity", "crossbeam-channel", @@ -1356,7 +1376,7 @@ dependencies = [ "executors", "futures", "hierarchical_hash_wheel_timer", - "hocon", + "humantime", "ipnet", "iprange", "kompact-actor-derive", @@ -1377,6 +1397,7 @@ dependencies = [ "slog-term", "synchronoise", "tempfile", + "toml", "trybuild", "uuid", ] @@ -1424,12 +1445,6 @@ dependencies = [ "log", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "leb128fmt" version = "0.1.0" @@ -1527,16 +1542,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "nom" -version = "4.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" -dependencies = [ - "memchr", - "version_check 0.1.5", -] - [[package]] name = "num-conv" version = "0.2.1" @@ -1739,6 +1744,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro-hack" version = "0.5.20+deprecated" @@ -1822,6 +1836,26 @@ dependencies = [ "protobuf-codegen", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quote" version = "1.0.45" @@ -1843,6 +1877,12 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -1957,13 +1997,33 @@ dependencies = [ "bitflags", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "regex" version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ - "aho-corasick 1.1.4", + "aho-corasick", "memchr", "regex-automata", "regex-syntax", @@ -1975,7 +2035,7 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ - "aho-corasick 1.1.4", + "aho-corasick", "memchr", "regex-syntax", ] @@ -1986,12 +2046,67 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "resolv-conf" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rust_decimal" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", + "wasm-bindgen", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -2048,12 +2163,30 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "semver" version = "1.0.28" @@ -2109,17 +2242,6 @@ dependencies = [ "zmij", ] -[[package]] -name = "serde_path_to_error" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" -dependencies = [ - "itoa", - "serde", - "serde_core", -] - [[package]] name = "serde_spanned" version = "1.1.1" @@ -2151,6 +2273,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "sized-chunks" version = "0.6.5" @@ -2285,6 +2413,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "target-triple" version = "1.0.0" @@ -2450,6 +2584,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + [[package]] name = "toml_parser" version = "1.1.2+spec-1.1.0" @@ -2557,6 +2703,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf8-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -2581,12 +2733,6 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" -[[package]] -name = "version_check" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" - [[package]] name = "version_check" version = "0.9.5" @@ -2941,6 +3087,9 @@ name = "winnow" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +dependencies = [ + "memchr", +] [[package]] name = "wit-bindgen" @@ -3036,6 +3185,15 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "yoke" version = "0.8.2" diff --git a/core/Cargo.toml b/core/Cargo.toml index cf9ef8a3..2409418a 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -69,7 +69,9 @@ slog = "2" slog-async = "2" slog-term = "2" rustc-hash = "2" -hocon = { version = "0.9", default-features = false } +toml = "1" +humantime = "2" +byte-unit = "5" hierarchical_hash_wheel_timer = "1.4" owning_ref = "0.4" futures = "0.3" diff --git a/core/src/component/context.rs b/core/src/component/context.rs index 3a416e2b..d6ad14a6 100644 --- a/core/src/component/context.rs +++ b/core/src/component/context.rs @@ -1,6 +1,9 @@ use super::*; -use crate::net::buffers::{BufferConfig, ChunkAllocator, ChunkRef}; +use crate::{ + config::Config, + net::buffers::{BufferConfig, ChunkAllocator, ChunkRef}, +}; use std::task::Poll; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] @@ -33,7 +36,7 @@ struct ComponentContextInner { pub(super) component: Weak>, logger: KompactLogger, actor_ref: ActorRef, - config: Arc, + config: Arc, id: Uuid, } @@ -154,7 +157,7 @@ where /// Handled::Ok /// } /// } - /// let default_values = r#"{ a = 7 }"#; + /// let default_values = r#"a = 7"#; /// let mut conf = KompactConfig::default(); /// conf.load_config_str(default_values); /// let system = conf.build().expect("system"); @@ -162,7 +165,7 @@ where /// system.start(&c); /// system.await_termination(); /// ``` - pub fn config(&self) -> &Hocon { + pub fn config(&self) -> &Config { self.inner_ref().config.as_ref() } @@ -353,7 +356,7 @@ where /// If buffers have already been initialized, explicitly or implicitly, the method does nothing. /// /// A custom [BufferConfig](net::buffers::BufferConfig) may be specified, if `None` is given the - /// actor will first try to parse the configuration from the system wide `Hocon`-Configuration, + /// actor will first try to parse the configuration from the system wide configuration, /// if that fails, the default config will be used. /// /// A custom [ChunkAllocator](net::buffers::ChunkAllocator) may also be specified. diff --git a/core/src/component/mod.rs b/core/src/component/mod.rs index 06dd2b58..c5625df6 100644 --- a/core/src/component/mod.rs +++ b/core/src/component/mod.rs @@ -1,4 +1,3 @@ -use hocon::Hocon; use std::{ cell::RefCell, fmt, diff --git a/core/src/config/converters_for_hocon_types.rs b/core/src/config/converters_for_config_values.rs similarity index 71% rename from core/src/config/converters_for_hocon_types.rs rename to core/src/config/converters_for_config_values.rs index 424a329f..ffd5ca78 100644 --- a/core/src/config/converters_for_hocon_types.rs +++ b/core/src/config/converters_for_config_values.rs @@ -6,13 +6,13 @@ pub struct StringValue; impl ConfigValueType for StringValue { type Value = String; - fn from_conf(conf: &Hocon) -> Result { + fn from_conf(conf: &ConfigValue) -> Result { conf.as_string() .ok_or_else(|| ConfigError::expected::(conf)) } - fn config_string(value: Self::Value) -> String { - format!(r#""{}""#, value) + fn into_config_value(value: Self::Value) -> ConfigValue { + ConfigValue::string(value) } } @@ -21,13 +21,13 @@ pub struct IntegerValue; impl ConfigValueType for IntegerValue { type Value = i64; - fn from_conf(conf: &Hocon) -> Result { + fn from_conf(conf: &ConfigValue) -> Result { conf.as_i64() .ok_or_else(|| ConfigError::expected::(conf)) } - fn config_string(value: Self::Value) -> String { - format!("{}", value) + fn into_config_value(value: Self::Value) -> ConfigValue { + ConfigValue::integer(value) } } @@ -36,13 +36,13 @@ pub struct RealValue; impl ConfigValueType for RealValue { type Value = f64; - fn from_conf(conf: &Hocon) -> Result { + fn from_conf(conf: &ConfigValue) -> Result { conf.as_f64() .ok_or_else(|| ConfigError::expected::(conf)) } - fn config_string(value: Self::Value) -> String { - format!("{}", value) + fn into_config_value(value: Self::Value) -> ConfigValue { + ConfigValue::real(value) } } @@ -51,13 +51,13 @@ pub struct BooleanValue; impl ConfigValueType for BooleanValue { type Value = bool; - fn from_conf(conf: &Hocon) -> Result { + fn from_conf(conf: &ConfigValue) -> Result { conf.as_bool() .ok_or_else(|| ConfigError::expected::(conf)) } - fn config_string(value: Self::Value) -> String { - format!("{}", value) + fn into_config_value(value: Self::Value) -> ConfigValue { + ConfigValue::boolean(value) } } @@ -66,13 +66,13 @@ pub struct BytesValue; impl ConfigValueType for BytesValue { type Value = u64; - fn from_conf(conf: &Hocon) -> Result { + fn from_conf(conf: &ConfigValue) -> Result { conf.as_bytes() .ok_or_else(|| ConfigError::expected::(conf)) } - fn config_string(value: Self::Value) -> String { - format!(r#""{}B""#, value) + fn into_config_value(value: Self::Value) -> ConfigValue { + ConfigValue::string(format!("{}B", value)) } } @@ -81,13 +81,13 @@ pub struct DurationValue; impl ConfigValueType for DurationValue { type Value = Duration; - fn from_conf(conf: &Hocon) -> Result { + fn from_conf(conf: &ConfigValue) -> Result { conf.as_duration() .ok_or_else(|| ConfigError::expected::(conf)) } - fn config_string(value: Self::Value) -> String { - format!(r#""{}ms""#, value.as_millis()) + fn into_config_value(value: Self::Value) -> ConfigValue { + ConfigValue::string(humantime::format_duration(value).to_string()) } } @@ -105,8 +105,8 @@ impl Default for ArrayOfValues { impl ConfigValueType for ArrayOfValues { type Value = Vec; - fn from_conf(conf: &Hocon) -> Result { - if let Hocon::Array(values) = conf { + fn from_conf(conf: &ConfigValue) -> Result { + if let ConfigValueInner::Array(values) = &conf.inner { values .iter() .try_fold(Vec::with_capacity(values.len()), |mut acc, c| { @@ -120,9 +120,8 @@ impl ConfigValueType for ArrayOfValues { } } - fn config_string(value: Self::Value) -> String { - let formatted: Vec = value.into_iter().map(T::config_string).collect(); - format!("[{}]", formatted.join(", ")) + fn into_config_value(value: Self::Value) -> ConfigValue { + ConfigValue::array(value.into_iter().map(T::into_config_value).collect()) } } @@ -153,7 +152,7 @@ mod tests { #[test] fn test_bytes() { - let conf = str_conf("size = 1.5KiB"); + let conf = str_conf(r#"size = "1.5KiB""#); let res = BytesValue::from_conf(&conf["size"]); assert_eq!(Ok(1536), res); } @@ -165,7 +164,7 @@ mod tests { #[test] fn test_duration() { - let conf = str_conf("time = 3days"); + let conf = str_conf(r#"time = "3days""#); let res = DurationValue::from_conf(&conf["time"]); assert_eq!(Ok(Duration::from_secs(3 * 24 * 60 * 60)), res); } diff --git a/core/src/config/converters_for_other_types.rs b/core/src/config/converters_for_other_types.rs index e522e9de..503c431c 100644 --- a/core/src/config/converters_for_other_types.rs +++ b/core/src/config/converters_for_other_types.rs @@ -5,7 +5,7 @@ pub struct UsizeValue; impl ConfigValueType for UsizeValue { type Value = usize; - fn from_conf(conf: &Hocon) -> Result { + fn from_conf(conf: &ConfigValue) -> Result { let res = conf .as_i64() .ok_or_else(|| ConfigError::expected::(conf))?; @@ -13,8 +13,8 @@ impl ConfigValueType for UsizeValue { Ok(ures) } - fn config_string(value: Self::Value) -> String { - format!("{}", value) + fn into_config_value(value: Self::Value) -> ConfigValue { + ConfigValue::integer(value.try_into().expect("usize should fit into i64")) } } @@ -23,14 +23,14 @@ pub struct F32Value; impl ConfigValueType for F32Value { type Value = f32; - fn from_conf(conf: &Hocon) -> Result { + fn from_conf(conf: &ConfigValue) -> Result { conf.as_f64() - .map(|v| v as f32) // this is safe...only loses accuracy + .map(|v| v as f32) .ok_or_else(|| ConfigError::expected::(conf)) } - fn config_string(value: Self::Value) -> String { - format!("{}", value) + fn into_config_value(value: Self::Value) -> ConfigValue { + ConfigValue::real(value.into()) } } @@ -41,7 +41,7 @@ mod tests { #[test] fn test_whole_bytes() { - let conf = str_conf("size = 1.5KiB"); + let conf = str_conf(r#"size = "1.5KiB""#); let res = BytesValue::from_conf(&conf["size"]); assert_eq!(Ok(1536u64), res); } diff --git a/core/src/config/mod.rs b/core/src/config/mod.rs index 8770bbae..60a58197 100644 --- a/core/src/config/mod.rs +++ b/core/src/config/mod.rs @@ -1,49 +1,841 @@ -use hocon::Hocon; -use std::{convert::TryInto, error::Error, fmt, marker::PhantomData}; +use std::{ + collections::HashMap, + convert::TryInto, + error::Error, + fmt, + marker::PhantomData, + ops::Index, + path::Path, +}; #[macro_use] mod macros; -mod converters_for_hocon_types; -pub use converters_for_hocon_types::*; +mod converters_for_config_values; +pub use converters_for_config_values::*; mod converters_for_other_types; pub use converters_for_other_types::*; const PATH_SEP: char = '.'; -/// Extension methods for Hocon instances to support [ConfigEntry](ConfigEntry) lookup. -pub trait HoconExt { - /// Read the value at the location given by `key` from this config. - fn get(&self, key: &ConfigEntry) -> Result - where - T: ConfigValueType; +type ConfigTable = HashMap; - /// Read the value at the location given by `key` from this config, or return the default, if any. - fn get_or_default(&self, key: &ConfigEntry) -> Result - where - T: ConfigValueType; +/// A parsed Kompact configuration document. +/// +/// The fallible lookup API is exposed via [Config::get] and [Config::select]. +/// For convenience, this type also supports panicking indexing with `[]`. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Config { + root: ConfigTable, } -impl HoconExt for Hocon { - fn get(&self, key: &ConfigEntry) -> Result +impl Config { + /// Create an empty configuration document. + pub fn new() -> Self { + Self::default() + } + + fn from_toml_table(table: toml::Table) -> Self { + Config { + root: table + .into_iter() + .map(|(key, value)| (key, ConfigValue::from_toml(value))) + .collect(), + } + } + + pub(crate) fn merge(&mut self, other: Config) { + merge_tables(&mut self.root, other.root); + } + + /// Select a single top-level key from the configuration. + pub fn get<'config, 'path>(&'config self, key: &'path str) -> ConfigLookup<'config, 'path> { + match self.root.get(key) { + Some(value) => ConfigLookup::from_full_path(key, value), + None => ConfigLookup::error_path(key, LookupErrorKind::MissingKey), + } + } + + /// Select a dotted path from the configuration. + pub fn select<'config, 'path>(&'config self, path: &'path str) -> ConfigLookup<'config, 'path> { + let mut segments = path.split(PATH_SEP); + let Some(first) = segments.next() else { + return ConfigLookup::error_path(path, LookupErrorKind::InvalidPath); + }; + if first.is_empty() { + return ConfigLookup::error_path(path, LookupErrorKind::InvalidPath); + } + + let Some(mut value) = self.root.get(first) else { + return ConfigLookup::error_path(path, LookupErrorKind::MissingKey); + }; + for segment in segments { + if segment.is_empty() { + return ConfigLookup::error_path(path, LookupErrorKind::InvalidPath); + } + value = match &value.inner { + ConfigValueInner::Table(values) => match values.get(segment) { + Some(value) => value, + None => return ConfigLookup::error_path(path, LookupErrorKind::MissingKey), + }, + _ => { + return ConfigLookup::error_path( + path, + LookupErrorKind::InvalidKeyAccess { + key: segment, + value_type: value.value_type_name(), + }, + ); + } + }; + } + ConfigLookup::from_full_path(path, value) + } + + /// Read a typed config entry from this config. + pub fn read(&self, key: &ConfigEntry) -> Result where T: ConfigValueType, { key.read(self) } - fn get_or_default(&self, key: &ConfigEntry) -> Result + /// Read a typed config entry from this config, or fall back to its default value. + pub fn read_or_default(&self, key: &ConfigEntry) -> Result where T: ConfigValueType, { key.read_or_default(self) } + + pub(crate) fn insert_value(&mut self, key: &str, value: ConfigValue) { + let segments: Vec<&str> = key.split(PATH_SEP).collect(); + insert_segments(&mut self.root, &segments, value); + } +} + +impl Index<&str> for Config { + type Output = ConfigValue; + + fn index(&self, key: &str) -> &Self::Output { + self.root + .get(key) + .unwrap_or_else(|| panic!("missing config key `{}`", key)) + } +} + +/// A parsed configuration value. +/// +/// Use [Config::get] or [Config::select] for path-aware fallible lookups with detailed +/// error messages. The indexing API on this type is intentionally panicking and is meant +/// only as a convenience shorthand. +#[derive(Debug, Clone, PartialEq)] +pub struct ConfigValue { + pub(crate) inner: ConfigValueInner, +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum ConfigValueInner { + String(String), + Integer(i64), + Real(f64), + Boolean(bool), + DateTime(toml::value::Datetime), + Array(Vec), + Table(ConfigTable), +} + +impl ConfigValue { + pub(crate) fn string(value: String) -> Self { + ConfigValue { + inner: ConfigValueInner::String(value), + } + } + + pub(crate) fn integer(value: i64) -> Self { + ConfigValue { + inner: ConfigValueInner::Integer(value), + } + } + + pub(crate) fn real(value: f64) -> Self { + ConfigValue { + inner: ConfigValueInner::Real(value), + } + } + + pub(crate) fn boolean(value: bool) -> Self { + ConfigValue { + inner: ConfigValueInner::Boolean(value), + } + } + + pub(crate) fn datetime(value: toml::value::Datetime) -> Self { + ConfigValue { + inner: ConfigValueInner::DateTime(value), + } + } + + pub(crate) fn array(values: Vec) -> Self { + ConfigValue { + inner: ConfigValueInner::Array(values), + } + } + + pub(crate) fn table() -> Self { + ConfigValue { + inner: ConfigValueInner::Table(HashMap::new()), + } + } + + fn from_toml(value: toml::Value) -> Self { + match value { + toml::Value::String(v) => ConfigValue::string(v), + toml::Value::Integer(v) => ConfigValue::integer(v), + toml::Value::Float(v) => ConfigValue::real(v), + toml::Value::Boolean(v) => ConfigValue::boolean(v), + toml::Value::Datetime(v) => ConfigValue::datetime(v), + toml::Value::Array(values) => { + ConfigValue::array(values.into_iter().map(ConfigValue::from_toml).collect()) + } + toml::Value::Table(values) => ConfigValue { + inner: ConfigValueInner::Table( + values + .into_iter() + .map(|(key, value)| (key, ConfigValue::from_toml(value))) + .collect(), + ), + }, + } + } + + pub(crate) fn to_toml_value(&self) -> toml::Value { + match &self.inner { + ConfigValueInner::String(v) => toml::Value::String(v.clone()), + ConfigValueInner::Integer(v) => toml::Value::Integer(*v), + ConfigValueInner::Real(v) => toml::Value::Float(*v), + ConfigValueInner::Boolean(v) => toml::Value::Boolean(*v), + ConfigValueInner::DateTime(v) => toml::Value::Datetime(*v), + ConfigValueInner::Array(values) => { + toml::Value::Array(values.iter().map(ConfigValue::to_toml_value).collect()) + } + ConfigValueInner::Table(values) => toml::Value::Table( + values + .iter() + .map(|(key, value)| (key.clone(), value.to_toml_value())) + .collect(), + ), + } + } + + /// Render this value as a TOML fragment. + pub fn to_toml_fragment(&self) -> String { + self.to_toml_value().to_string() + } + + /// Return the value as a string if possible. + pub fn as_string(&self) -> Option { + match &self.inner { + ConfigValueInner::String(v) => Some(v.clone()), + _ => None, + } + } + + /// Return the value as an integer if possible. + pub fn as_i64(&self) -> Option { + match &self.inner { + ConfigValueInner::Integer(v) => Some(*v), + _ => None, + } + } + + /// Return the value as a float if possible. + pub fn as_f64(&self) -> Option { + match &self.inner { + ConfigValueInner::Integer(v) => Some(*v as f64), + ConfigValueInner::Real(v) => Some(*v), + _ => None, + } + } + + /// Return the value as a boolean if possible. + pub fn as_bool(&self) -> Option { + match &self.inner { + ConfigValueInner::Boolean(v) => Some(*v), + _ => None, + } + } + + /// Return the value as a TOML datetime if possible. + pub fn as_datetime(&self) -> Option<&toml::value::Datetime> { + match &self.inner { + ConfigValueInner::DateTime(v) => Some(v), + _ => None, + } + } + + /// Return the value as a byte size if possible. + pub fn as_bytes(&self) -> Option { + match &self.inner { + ConfigValueInner::Integer(v) if *v >= 0 => Some(*v as u64), + ConfigValueInner::String(v) => byte_unit::Byte::parse_str(v, true) + .ok() + .map(|bytes| bytes.as_u64()), + _ => None, + } + } + + /// Return the value as a duration if possible. + pub fn as_duration(&self) -> Option { + match &self.inner { + ConfigValueInner::String(v) => humantime::parse_duration(v).ok(), + _ => None, + } + } + + fn value_type_name(&self) -> &'static str { + match &self.inner { + ConfigValueInner::String(_) => "string", + ConfigValueInner::Integer(_) => "integer", + ConfigValueInner::Real(_) => "float", + ConfigValueInner::Boolean(_) => "boolean", + ConfigValueInner::DateTime(_) => "datetime", + ConfigValueInner::Array(_) => "array", + ConfigValueInner::Table(_) => "table", + } + } + + fn merge(&mut self, other: ConfigValue) { + match (&mut self.inner, other.inner) { + (ConfigValueInner::Table(current), ConfigValueInner::Table(next)) => { + merge_tables(current, next); + } + (current, next) => *current = next, + } + } +} + +impl Index<&str> for ConfigValue { + type Output = ConfigValue; + + fn index(&self, key: &str) -> &Self::Output { + match &self.inner { + ConfigValueInner::Table(values) => values + .get(key) + .unwrap_or_else(|| panic!("missing config key `{}`", key)), + _ => panic!( + "cannot index config key `{}` into {} value", + key, + self.value_type_name() + ), + } + } +} + +impl Index for ConfigValue { + type Output = ConfigValue; + + fn index(&self, index: usize) -> &Self::Output { + match &self.inner { + ConfigValueInner::Array(values) => values.get(index).unwrap_or_else(|| { + panic!( + "index out of bounds: the len is {} but the index is {}", + values.len(), + index + ) + }), + _ => panic!( + "cannot index config index [{}] into {} value", + index, + self.value_type_name() + ), + } + } +} + +#[derive(Debug, Clone, Copy)] +enum PathSegment<'path> { + Key(&'path str), + Index(usize), +} + +impl fmt::Display for PathSegment<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PathSegment::Key(key) => write!(f, "{}", key), + PathSegment::Index(index) => write!(f, "[{}]", index), + } + } +} + +#[derive(Clone, Copy)] +enum LookupPathSource<'path> { + FullPath(&'path str), + Child { + parent: &'path dyn fmt::Display, + segment: PathSegment<'path>, + prepend_separator: bool, + }, +} + +impl fmt::Display for LookupPathSource<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LookupPathSource::FullPath(path) => write!(f, "{}", path), + LookupPathSource::Child { + parent, + segment, + prepend_separator, + } => { + write!(f, "{}", parent)?; + if *prepend_separator { + write!(f, "{}", PATH_SEP)?; + } + write!(f, "{}", segment) + } + } + } +} + +impl fmt::Debug for LookupPathSource<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "LookupPathSource({})", self) + } +} + +#[derive(Debug, Clone, Copy)] +enum LookupErrorKind<'path> { + InvalidPath, + MissingKey, + MissingIndex, + InvalidKeyAccess { + key: &'path str, + value_type: &'static str, + }, + InvalidIndexAccess { + index: usize, + value_type: &'static str, + }, +} + +/// A fallible config path lookup that carries the traversed path for later conversions. +/// +/// - `'config`: lifetime of the selected value borrowed from the underlying config tree. +/// - `'path`: lifetime of any borrowed path fragments and the parent lookup chain that are +/// used to render detailed path errors lazily. +#[derive(Debug, Clone, Copy)] +pub struct ConfigLookup<'config, 'path> { + inner: ConfigLookupInner<'config, 'path>, +} + +#[derive(Debug, Clone, Copy)] +enum ConfigLookupInner<'config, 'path> { + Value { + value: &'config ConfigValue, + path: LookupPathSource<'path>, + }, + Error { + kind: LookupErrorKind<'path>, + path: LookupPathSource<'path>, + }, +} + +impl ConfigLookup<'_, '_> { + fn is_empty_path(&self) -> bool { + match self.inner { + ConfigLookupInner::Value { path, .. } | ConfigLookupInner::Error { path, .. } => { + matches!(path, LookupPathSource::FullPath("")) + } + } + } +} + +impl fmt::Display for ConfigLookup<'_, '_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.inner { + ConfigLookupInner::Value { path, .. } | ConfigLookupInner::Error { path, .. } => { + write!(f, "{}", path) + } + } + } +} + +impl<'config, 'path> ConfigLookup<'config, 'path> { + fn from_full_path(path: &'path str, value: &'config ConfigValue) -> Self { + ConfigLookup { + inner: ConfigLookupInner::Value { + value, + path: LookupPathSource::FullPath(path), + }, + } + } + + fn error_path(path: &'path str, kind: LookupErrorKind<'path>) -> Self { + ConfigLookup { + inner: ConfigLookupInner::Error { + kind, + path: LookupPathSource::FullPath(path), + }, + } + } + + fn path_error(&self) -> ConfigPathError { + match self.inner { + ConfigLookupInner::Value { .. } => { + unreachable!("value lookups do not produce path errors") + } + ConfigLookupInner::Error { + kind: LookupErrorKind::InvalidPath, + .. + } => ConfigPathError::InvalidPath { + path: self.to_string(), + }, + ConfigLookupInner::Error { + kind: LookupErrorKind::MissingKey, + .. + } => ConfigPathError::MissingKey { + path: self.to_string(), + }, + ConfigLookupInner::Error { + kind: LookupErrorKind::MissingIndex, + .. + } => ConfigPathError::MissingIndex { + path: self.to_string(), + }, + ConfigLookupInner::Error { + kind: LookupErrorKind::InvalidKeyAccess { key, value_type }, + .. + } => ConfigPathError::InvalidKeyAccess { + path: self.to_string(), + key: key.to_string(), + value_type, + }, + ConfigLookupInner::Error { + kind: LookupErrorKind::InvalidIndexAccess { index, value_type }, + .. + } => ConfigPathError::InvalidIndexAccess { + path: self.to_string(), + index, + value_type, + }, + } + } + + /// Select a child key from the current lookup value. + pub fn get<'step>(&'step self, key: &'step str) -> ConfigLookup<'config, 'step> { + let path = LookupPathSource::Child { + parent: self, + segment: PathSegment::Key(key), + prepend_separator: !self.is_empty_path(), + }; + match self.inner { + ConfigLookupInner::Value { value, .. } => match &value.inner { + ConfigValueInner::Table(values) => match values.get(key) { + Some(value) => ConfigLookup { + inner: ConfigLookupInner::Value { value, path }, + }, + None => ConfigLookup { + inner: ConfigLookupInner::Error { + kind: LookupErrorKind::MissingKey, + path, + }, + }, + }, + _ => ConfigLookup { + inner: ConfigLookupInner::Error { + kind: LookupErrorKind::InvalidKeyAccess { + key, + value_type: value.value_type_name(), + }, + path, + }, + }, + }, + ConfigLookupInner::Error { kind, .. } => ConfigLookup { + inner: ConfigLookupInner::Error { kind, path }, + }, + } + } + + /// Select a child index from the current lookup value. + pub fn get_index<'step>(&'step self, index: usize) -> ConfigLookup<'config, 'step> { + let path = LookupPathSource::Child { + parent: self, + segment: PathSegment::Index(index), + prepend_separator: false, + }; + match self.inner { + ConfigLookupInner::Value { value, .. } => match &value.inner { + ConfigValueInner::Array(values) => match values.get(index) { + Some(value) => ConfigLookup { + inner: ConfigLookupInner::Value { value, path }, + }, + None => ConfigLookup { + inner: ConfigLookupInner::Error { + kind: LookupErrorKind::MissingIndex, + path, + }, + }, + }, + _ => ConfigLookup { + inner: ConfigLookupInner::Error { + kind: LookupErrorKind::InvalidIndexAccess { + index, + value_type: value.value_type_name(), + }, + path, + }, + }, + }, + ConfigLookupInner::Error { kind, .. } => ConfigLookup { + inner: ConfigLookupInner::Error { kind, path }, + }, + } + } + + /// Return the underlying value or the captured path error. + pub fn value(&self) -> Result<&'config ConfigValue, ConfigError> { + match self.inner { + ConfigLookupInner::Value { value, .. } => Ok(value), + ConfigLookupInner::Error { .. } => Err(ConfigError::PathError(self.path_error())), + } + } + + /// Return the selected value as a string. + pub fn as_string(&self) -> Result { + let value = self.value()?; + value + .as_string() + .ok_or_else(|| self.expected::(value)) + } + + /// Return the selected value as an integer. + pub fn as_i64(&self) -> Result { + let value = self.value()?; + value.as_i64().ok_or_else(|| self.expected::(value)) + } + + /// Return the selected value as a float. + pub fn as_f64(&self) -> Result { + let value = self.value()?; + value.as_f64().ok_or_else(|| self.expected::(value)) + } + + /// Return the selected value as a boolean. + pub fn as_bool(&self) -> Result { + let value = self.value()?; + value.as_bool().ok_or_else(|| self.expected::(value)) + } + + /// Return the selected value as a TOML datetime. + pub fn as_datetime(&self) -> Result<&'config toml::value::Datetime, ConfigError> { + let value = self.value()?; + value + .as_datetime() + .ok_or_else(|| self.expected::(value)) + } + + /// Return the selected value as a byte size. + pub fn as_bytes(&self) -> Result { + let value = self.value()?; + value.as_bytes().ok_or_else(|| self.expected::(value)) + } + + /// Return the selected value as a duration. + pub fn as_duration(&self) -> Result { + let value = self.value()?; + value + .as_duration() + .ok_or_else(|| self.expected::(value)) + } + + fn expected(&self, value: &ConfigValue) -> ConfigError { + match self.inner { + ConfigLookupInner::Value { .. } => { + ConfigError::expected_at::(&self.to_string(), value) + } + ConfigLookupInner::Error { .. } => ConfigError::PathError(self.path_error()), + } + } +} + +/// Errors raised while traversing a parsed configuration value. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ConfigPathError { + /// The requested path string is malformed. + InvalidPath { + /// The malformed config path. + path: String, + }, + /// The requested key was missing. + MissingKey { + /// The full missing config path. + path: String, + }, + /// The requested array element was missing. + MissingIndex { + /// The full missing config path. + path: String, + }, + /// A key access was attempted on a non-table value. + InvalidKeyAccess { + /// The full config path that was being traversed. + path: String, + /// The key fragment whose access failed. + key: String, + /// The type of the parent value that rejected the key access. + value_type: &'static str, + }, + /// An index access was attempted on a non-array value. + InvalidIndexAccess { + /// The full config path that was being traversed. + path: String, + /// The index fragment whose access failed. + index: usize, + /// The type of the parent value that rejected the index access. + value_type: &'static str, + }, +} + +impl ConfigPathError { + /// Returns `true` if the error denotes a missing path element. + pub fn is_missing(&self) -> bool { + matches!( + self, + ConfigPathError::MissingKey { .. } | ConfigPathError::MissingIndex { .. } + ) + } +} + +impl fmt::Display for ConfigPathError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ConfigPathError::InvalidPath { path } => write!(f, "invalid config path `{}`", path), + ConfigPathError::MissingKey { path } => write!(f, "missing config key at `{}`", path), + ConfigPathError::MissingIndex { path } => { + write!(f, "missing config index at `{}`", path) + } + ConfigPathError::InvalidKeyAccess { + path, + key, + value_type, + } => write!( + f, + "cannot access key `{}` at `{}` because the parent value is a {}", + key, path, value_type + ), + ConfigPathError::InvalidIndexAccess { + path, + index, + value_type, + } => write!( + f, + "cannot access index [{}] at `{}` because the parent value is a {}", + index, path, value_type + ), + } + } +} + +impl Error for ConfigPathError {} + +/// Errors raised while loading TOML configuration sources. +#[derive(Debug)] +pub enum ConfigLoadingError { + /// Reading a config file failed. + Io(std::io::Error), + /// Parsing TOML failed. + Parse(toml::de::Error), +} + +impl fmt::Display for ConfigLoadingError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ConfigLoadingError::Io(err) => write!(f, "failed to read config file: {}", err), + ConfigLoadingError::Parse(err) => write!(f, "failed to parse TOML config: {}", err), + } + } +} + +impl Error for ConfigLoadingError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + ConfigLoadingError::Io(err) => Some(err), + ConfigLoadingError::Parse(err) => Some(err), + } + } +} + +impl From for ConfigLoadingError { + fn from(error: std::io::Error) -> Self { + ConfigLoadingError::Io(error) + } +} + +impl From for ConfigLoadingError { + fn from(error: toml::de::Error) -> Self { + ConfigLoadingError::Parse(error) + } +} + +/// Parse a TOML string into a configuration document. +pub fn parse_config_str(config_string: &str) -> Result { + let config: toml::Table = toml::from_str(config_string)?; + Ok(Config::from_toml_table(config)) +} + +/// Parse a TOML file into a configuration document. +pub fn parse_config_file

(path: P) -> Result +where + P: AsRef, +{ + let config_string = std::fs::read_to_string(path)?; + parse_config_str(config_string.as_ref()) +} + +fn merge_tables(current: &mut ConfigTable, next: ConfigTable) { + for (key, value) in next { + if let Some(existing) = current.get_mut(&key) { + existing.merge(value); + } else { + current.insert(key, value); + } + } +} + +fn insert_segments(current: &mut ConfigTable, segments: &[&str], value: ConfigValue) { + if segments.is_empty() { + return; + } + + if segments.len() == 1 { + if let Some(existing) = current.get_mut(segments[0]) { + existing.merge(value); + } else { + current.insert(segments[0].to_string(), value); + } + return; + } + + let child = current + .entry(segments[0].to_string()) + .or_insert_with(ConfigValue::table); + + if !matches!(child.inner, ConfigValueInner::Table(_)) { + *child = ConfigValue::table(); + } + + let values = match &mut child.inner { + ConfigValueInner::Table(values) => values, + _ => unreachable!("table values should stay tables"), + }; + insert_segments(values, &segments[1..], value); } /// A validator for extracted values of `T::Value` pub type ValidatorFun = fn(&::Value) -> Result<(), String>; -/// Description of a configuration parameter that can be set via HOCON config. +/// Description of a configuration parameter that can be set via TOML config. /// /// - `T`: Type information of this config value. Can be used for converting the raw config value into a runtime type. /// @@ -55,7 +847,7 @@ pub struct ConfigEntry where T: ConfigValueType, { - /// The full path key to read this config value from a HOCON config. + /// The full path key to read this config value from a TOML config. pub key: &'static str, /// Documentation for this config entry. pub doc: &'static str, @@ -75,7 +867,7 @@ where { /// The default value for this config entry. /// - /// Used if no value is specified in the Hocon config. + /// Used if no value is specified in the config. pub fn default(&self) -> Option { self.default.map(|default_fn| default_fn()) } @@ -86,114 +878,187 @@ where } /// Select the entry corresponding to this key from the given config. - pub fn select<'a>(&self, conf: &'a Hocon) -> &'a Hocon { - let path = self.key.split(PATH_SEP); - let mut hocon = conf; - for segment in path { - hocon = &hocon[segment]; - } - hocon + pub fn select<'config>(&self, conf: &'config Config) -> ConfigLookup<'config, 'static> { + conf.select(self.key) } - /// Performs the validation of the given value + /// Performs the validation of the given value. /// /// If no validator is specified, the value is simply passed through. - pub fn validate(&self, v: T::Value) -> Result { + pub fn validate(&self, value: T::Value) -> Result { if let Some(validator) = self.validator { - match validator(&v) { - Ok(_) => Ok(v), - Err(err_msg) => Err(ConfigError::InvalidValue(err_msg)), + match validator(&value) { + Ok(_) => Ok(value), + Err(err_msg) => Err(ConfigError::InvalidValue { + path: Some(self.key.to_string()), + description: err_msg, + }), } } else { - Ok(v) + Ok(value) } } /// Read the value for this key from the given config. - pub fn read(&self, conf: &Hocon) -> Result { - let hocon = self.select(conf); - if let Hocon::BadValue(error) = hocon { - Err(error.clone().into()) - } else { - let v = T::from_conf(hocon)?; - self.validate(v) - } + pub fn read(&self, conf: &Config) -> Result { + let value = self.select(conf).value()?; + let value = T::from_conf(value).map_err(|error| error.with_path(self.key))?; + self.validate(value) } /// Read the value for this key from the given config or return the default value if the path is not present. - pub fn read_or_default(&self, conf: &Hocon) -> Result { + pub fn read_or_default(&self, conf: &Config) -> Result { match self.read(conf) { - Ok(v) => Ok(v), - Err(ConfigError::PathError(e)) => match self.default() { + Ok(value) => Ok(value), + Err(ConfigError::PathError(error)) if error.is_missing() => match self.default() { Some(default) => Ok(default), - None => Err(ConfigError::PathError(e)), + None => Err(ConfigError::PathError(error)), }, - Err(e) => Err(e), + Err(error) => Err(error), } } } -/// A value extractor for config values +/// A value extractor for config values. pub trait ConfigValueType { /// The type of the value extracted by this type. type Value; /// Extract the value from a config instance. - fn from_conf(conf: &Hocon) -> Result; + fn from_conf(conf: &ConfigValue) -> Result; + + /// Convert a runtime value into a config value. + fn into_config_value(value: Self::Value) -> ConfigValue; - /// Produce a format for the value that can be spliced into a config string. - fn config_string(value: Self::Value) -> String; + /// Produce a TOML fragment for the value. + fn config_string(value: Self::Value) -> String { + Self::into_config_value(value).to_toml_fragment() + } } /// Errors that occur during config lookup. #[derive(Debug, Clone, PartialEq)] pub enum ConfigError { /// Type conversion failed. - ConversionError(String), + ConversionError { + /// The config path where the conversion was attempted, if known. + path: Option, + /// A human-readable description of the failure. + description: String, + }, /// Path traversal failed. - PathError(hocon::Error), + PathError(ConfigPathError), /// Value validation failed. - InvalidValue(String), + InvalidValue { + /// The config path that failed validation, if known. + path: Option, + /// A human-readable description of the failure. + description: String, + }, } + impl ConfigError { - fn expected(conf: &Hocon) -> Self { - let descr = format!( - "Expected {} config value, but got {:?}", - std::any::type_name::(), - conf - ); - ConfigError::ConversionError(descr) + fn expected(conf: &ConfigValue) -> Self { + ConfigError::ConversionError { + path: None, + description: format!( + "Expected {} config value, but got {:?}", + std::any::type_name::(), + conf + ), + } + } + + fn expected_at(path: &str, conf: &ConfigValue) -> Self { + ConfigError::ConversionError { + path: Some(path.to_string()), + description: format!( + "Expected {} config value, but got {:?}", + std::any::type_name::(), + conf + ), + } + } + + fn with_path(self, path: &str) -> Self { + match self { + ConfigError::ConversionError { + path: Some(existing), + description, + } => ConfigError::ConversionError { + path: Some(existing), + description, + }, + ConfigError::ConversionError { + path: None, + description, + } => ConfigError::ConversionError { + path: Some(path.to_string()), + description, + }, + ConfigError::InvalidValue { + path: Some(existing), + description, + } => ConfigError::InvalidValue { + path: Some(existing), + description, + }, + ConfigError::InvalidValue { + path: None, + description, + } => ConfigError::InvalidValue { + path: Some(path.to_string()), + description, + }, + other => other, + } } } -impl From for ConfigError { - fn from(error: hocon::Error) -> Self { + +impl From for ConfigError { + fn from(error: ConfigPathError) -> Self { ConfigError::PathError(error) } } + impl From for ConfigError { fn from(error: std::num::TryFromIntError) -> Self { - ConfigError::ConversionError(format!("{}", error)) + ConfigError::ConversionError { + path: None, + description: error.to_string(), + } } } + impl fmt::Display for ConfigError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ConfigError::ConversionError(description) => { - write!(f, "Error during type conversion: {}", description) - } + ConfigError::ConversionError { path, description } => match path { + Some(path) => write!( + f, + "Error during type conversion at `{}`: {}", + path, description + ), + None => write!(f, "Error during type conversion: {}", description), + }, ConfigError::PathError(error) => write!(f, "Error during path traversal: {}", error), - ConfigError::InvalidValue(description) => { - write!(f, "Error during value validation: {}", description) - } + ConfigError::InvalidValue { path, description } => match path { + Some(path) => write!( + f, + "Error during value validation at `{}`: {}", + path, description + ), + None => write!(f, "Error during value validation: {}", description), + }, } } } + impl Error for ConfigError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { - ConfigError::ConversionError(_) => None, ConfigError::PathError(error) => Some(error), - ConfigError::InvalidValue(_) => None, + ConfigError::ConversionError { .. } | ConfigError::InvalidValue { .. } => None, } } } @@ -201,7 +1066,6 @@ impl Error for ConfigError { #[cfg(test)] mod tests { use super::*; - use hocon::HoconLoader; const SIMPLE_KEY: ConfigEntry = ConfigEntry { key: "kompact.my-test-key", @@ -254,20 +1118,19 @@ mod tests { } const EXAMPLE_CONFIG: &str = r#" - kompact { - my-test-key = "testme", - - my-validate-key = 50 + [kompact] + my-test-key = "testme" + my-validate-key = 50 - test-group { - inner-key = "test me inside" - } - } + [kompact.test-group] + inner-key = "test me inside" "#; const BAD_CONFIG: &str = r#" my-test-key = "testme" - kompact.my-validate-key = 200 + + [kompact] + my-validate-key = 200 "#; #[test] @@ -275,55 +1138,99 @@ mod tests { assert_eq!("kompact.my-test-key", SIMPLE_KEY.key); assert_eq!(vec!["kompact", "my-test-key"], SIMPLE_KEY.path_segments()); - let loader = HoconLoader::new().load_str(EXAMPLE_CONFIG).expect("config"); - let hocon = loader.hocon().expect("config"); - let v = SIMPLE_KEY.read(&hocon).expect("String"); - assert_eq!("testme", v); + let conf = parse_config_str(EXAMPLE_CONFIG).expect("config"); + let value = SIMPLE_KEY.read(&conf).expect("String"); + assert_eq!("testme", value); - let v2 = KEY_FROM_MACRO_NO_DEFAULT.read(&hocon).expect("String"); - assert_eq!("test me inside", v2); + let nested = KEY_FROM_MACRO_NO_DEFAULT.read(&conf).expect("String"); + assert_eq!("test me inside", nested); } #[test] fn default_config_key() { - let loader = HoconLoader::new().load_str(EXAMPLE_CONFIG).expect("config"); - let hocon = loader.hocon().expect("config"); - let v = KEY_WITH_DEFAULT.read_or_default(&hocon).expect("String"); - assert_eq!("default string", v); + let conf = parse_config_str(EXAMPLE_CONFIG).expect("config"); + let value = KEY_WITH_DEFAULT.read_or_default(&conf).expect("String"); + assert_eq!("default string", value); - let v2 = KEY_FROM_MACRO.read_or_default(&hocon).expect("String"); - assert_eq!("default value", v2); + let nested = KEY_FROM_MACRO.read_or_default(&conf).expect("String"); + assert_eq!("default value", nested); } #[test] fn validated_config_key() { { - let loader = HoconLoader::new().load_str(EXAMPLE_CONFIG).expect("config"); - let hocon = loader.hocon().expect("config"); + let conf = parse_config_str(EXAMPLE_CONFIG).expect("config"); - let v = hocon.get(&KEY_FROM_MACRO_VALIDATE).unwrap(); - assert_eq!(50, v); + let value = conf.read(&KEY_FROM_MACRO_VALIDATE).unwrap(); + assert_eq!(50, value); } { - let loader = HoconLoader::new().load_str(BAD_CONFIG).expect("config"); - let hocon = loader.hocon().expect("config"); + let conf = parse_config_str(BAD_CONFIG).expect("config"); - let res = hocon.get(&KEY_FROM_MACRO_VALIDATE); + let res = conf.read(&KEY_FROM_MACRO_VALIDATE); assert!(res.is_err()); } } #[test] fn simple_key_bad_config() { - let loader = HoconLoader::new().load_str(BAD_CONFIG).expect("config"); - let hocon = loader.hocon().expect("config"); - let res = SIMPLE_KEY.read(&hocon); - assert_eq!(Err(ConfigError::PathError(hocon::Error::MissingKey)), res); + let conf = parse_config_str(BAD_CONFIG).expect("config"); + let res = SIMPLE_KEY.read(&conf); + assert_eq!( + Err(ConfigError::PathError(ConfigPathError::MissingKey { + path: "kompact.my-test-key".to_string() + })), + res + ); + } + + #[test] + fn lookup_reports_full_missing_path() { + let conf = parse_config_str(EXAMPLE_CONFIG).expect("config"); + let res = conf.select("kompact.test-group.other-key").as_string(); + assert_eq!( + Err(ConfigError::PathError(ConfigPathError::MissingKey { + path: "kompact.test-group.other-key".to_string() + })), + res + ); + } + + #[test] + fn lookup_reports_invalid_parent_type() { + let conf = parse_config_str(EXAMPLE_CONFIG).expect("config"); + let res = conf.select("kompact.my-test-key.inner").as_string(); + assert_eq!( + Err(ConfigError::PathError(ConfigPathError::InvalidKeyAccess { + path: "kompact.my-test-key.inner".to_string(), + key: "inner".to_string(), + value_type: "string" + })), + res + ); + } + + #[test] + fn lookup_propagates_additional_key_fragments_after_failure() { + let conf = parse_config_str(EXAMPLE_CONFIG).expect("config"); + let res = conf + .get("kompact") + .get("my-test-key") + .get("inner") + .get("leaf") + .as_string(); + assert_eq!( + Err(ConfigError::PathError(ConfigPathError::InvalidKeyAccess { + path: "kompact.my-test-key.inner.leaf".to_string(), + key: "inner".to_string(), + value_type: "string" + })), + res + ); } - pub(super) fn str_conf(config_string: &str) -> Hocon { - let loader = HoconLoader::new().load_str(config_string).expect("config"); - loader.hocon().expect("config") + pub(super) fn str_conf(config_string: &str) -> Config { + parse_config_str(config_string).expect("config") } const ROUNDTRIP_KEY: &str = "value"; @@ -333,10 +1240,7 @@ mod tests { T::Value: PartialEq + fmt::Debug + Clone, { let config_string = format!("{} = {}", ROUNDTRIP_KEY, T::config_string(value.clone())); - let loader = HoconLoader::new() - .load_str(config_string.as_ref()) - .expect("config"); - let conf = loader.hocon().expect("config"); + let conf = parse_config_str(config_string.as_ref()).expect("config"); let res = T::from_conf(&conf[ROUNDTRIP_KEY]); assert_eq!(Ok(value), res); } diff --git a/core/src/dispatch/lookup/gc.rs b/core/src/dispatch/lookup/gc.rs index 5fdc938b..4e661fb3 100644 --- a/core/src/dispatch/lookup/gc.rs +++ b/core/src/dispatch/lookup/gc.rs @@ -1,4 +1,4 @@ -use crate::dispatch::lookup::*; +use crate::{config::Config, dispatch::lookup::*}; use arc_swap::ArcSwap; mod defaults { @@ -67,9 +67,14 @@ impl ActorRefReaper { } } - pub fn from_config(conf: &hocon::Hocon) -> Self { + pub fn from_config(conf: &Config) -> Self { // TODO(Adam): Make all parameters configurable - let algorithm = match conf[config_keys::ALGORITHM].as_string().as_deref() { + let algorithm = match conf + .select(config_keys::ALGORITHM) + .as_string() + .ok() + .as_deref() + { Some("AIMD") => FeedbackAlgorithm::Aimd, Some("MIMD") => FeedbackAlgorithm::Mimd, Some(s) => panic!( diff --git a/core/src/lib.rs b/core/src/lib.rs index 0a60555b..6592ab29 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1262,7 +1262,7 @@ mod tests { #[test] fn test_config_from_string() -> () { - let default_values = r#"{ a = 7 }"#; + let default_values = r#"a = 7"#; let mut conf = KompactConfig::default(); conf.load_config_str(default_values); let system = conf.build().expect("system"); @@ -1276,12 +1276,10 @@ mod tests { #[test] fn test_config_from_file() -> () { - //let default_values = r#"{ a = 7 }"#; - let config_file_path = Fixture::blank("application.conf"); + //let default_values = r#"a = 7"#; + let config_file_path = Fixture::blank("test_settings.toml"); let mut config_file = File::create(config_file_path.deref()).expect("config file"); - config_file - .write_all(b"{ a = 7 }") - .expect("write config file"); + config_file.write_all(b"a = 7").expect("write config file"); let mut conf = KompactConfig::default(); conf.load_config_file(config_file_path.to_path_buf()); let system = conf.build().expect("system"); @@ -1295,12 +1293,10 @@ mod tests { #[test] fn test_config_merged() -> () { - let default_values = r#"{ a = 5 }"#; - let config_file_path = Fixture::blank("application.conf"); + let default_values = r#"a = 5"#; + let config_file_path = Fixture::blank("test_settings.toml"); let mut config_file = File::create(config_file_path.deref()).expect("config file"); - config_file - .write_all(b"{ a = 7 }") - .expect("write config file"); + config_file.write_all(b"a = 7").expect("write config file"); let mut conf = KompactConfig::default(); conf.load_config_str(default_values) .load_config_file(config_file_path.to_path_buf()); diff --git a/core/src/net/buffers/encode_buffer.rs b/core/src/net/buffers/encode_buffer.rs index 6288101b..adf1bcb4 100644 --- a/core/src/net/buffers/encode_buffer.rs +++ b/core/src/net/buffers/encode_buffer.rs @@ -332,8 +332,8 @@ unsafe impl BufMut for BufferEncoder<'_> { #[cfg(test)] mod tests { use super::*; + use crate::config::parse_config_str; use bytes::Bytes; - use hocon::HoconLoader; // This test instantiates an EncodeBuffer and writes the same string into it enough times that // the EncodeBuffer should overload multiple times and will have to succeed in reusing >=1 Chunk @@ -391,23 +391,20 @@ mod tests { ); } - // As above, except we create small buffers from a Hocon config. + // As above, except we create small buffers from a TOML config. #[test] - fn encode_buffer_overload_reuse_hocon_configured_small_buffers() { - let hocon = HoconLoader::new() - .load_str( - r#"{ - buffer_config { - chunk_size: 128, - initial_chunk_count: 2, - max_pool_count: 2, - encode_min_remaining: 2, - } - }"#, - ) - .unwrap() - .hocon(); - let buffer_config = BufferConfig::from_config(&hocon.unwrap()); + fn encode_buffer_overload_reuse_toml_configured_small_buffers() { + let config = parse_config_str( + r#" + [buffer_config] + chunk_size = 128 + initial_chunk_count = 2 + max_chunk_count = 2 + encode_min_remaining = 2 + "#, + ) + .unwrap(); + let buffer_config = BufferConfig::from_config(&config); // Ensure we successfully parsed the Config assert_eq!(buffer_config.encode_buf_min_free_space, 2usize); let data_len = buffer_config.chunk_size - buffer_config.encode_buf_min_free_space - 8; diff --git a/core/src/net/buffers/mod.rs b/core/src/net/buffers/mod.rs index 5420fa76..2ee2780b 100644 --- a/core/src/net/buffers/mod.rs +++ b/core/src/net/buffers/mod.rs @@ -1,8 +1,10 @@ use super::*; -use crate::messaging::DispatchEnvelope; +use crate::{ + config::{Config, parse_config_file}, + messaging::DispatchEnvelope, +}; use bytes::{Buf, BufMut}; use core::{cmp, fmt, ptr}; -use hocon::{Hocon, HoconLoader}; use std::{ convert::{TryFrom, TryInto}, fmt::{Debug, Formatter}, @@ -37,24 +39,23 @@ impl BufferConfig { /// /// Returns the same values as [BufferConfig::default](BufferConfig::default) /// if it fails to read from the config. - pub fn from_config(config: &Hocon) -> Self { + pub fn from_config(config: &Config) -> Self { let mut buffer_config = BufferConfig::default(); - if let Some(chunk_size) = config["buffer_config"]["chunk_size"].as_bytes() { + let buffer_section = config.get("buffer_config"); + if let Ok(chunk_size) = buffer_section.get("chunk_size").as_bytes() { buffer_config.chunk_size = chunk_size .try_into() .expect("Invalid byte number for chunk_size"); } - if let Some(initial_chunk_count) = config["buffer_config"]["initial_chunk_count"].as_i64() { + if let Ok(initial_chunk_count) = buffer_section.get("initial_chunk_count").as_i64() { buffer_config.initial_chunk_count = usize::try_from(initial_chunk_count).expect("Invalid initial_chunk_count"); } - if let Some(max_chunk_count) = config["buffer_config"]["max_chunk_count"].as_i64() { + if let Ok(max_chunk_count) = buffer_section.get("max_chunk_count").as_i64() { buffer_config.max_chunk_count = usize::try_from(max_chunk_count).expect("Invalid max_chunk_count") } - if let Some(encode_min_remaining) = - config["buffer_config"]["encode_min_remaining"].as_bytes() - { + if let Ok(encode_min_remaining) = buffer_section.get("encode_min_remaining").as_bytes() { buffer_config.encode_buf_min_free_space = encode_min_remaining .try_into() .expect("Invalid byte number for encode_min_remaining"); @@ -63,24 +64,20 @@ impl BufferConfig { buffer_config } - /// Tries to deserialise a `BufferConfig` from a HOCON file at `path` + /// Tries to deserialise a `BufferConfig` from a TOML file at `path` /// /// Returns the same values as [BufferConfig::default](BufferConfig::default) /// if it fails to read from the config. /// /// # Panics /// - /// Panics if it fails to load the file or fails to load the file as HOCON. + /// Panics if it fails to load the file or fails to load the file as TOML. pub fn from_config_file

(path: P) -> BufferConfig where P: Into, { let p: PathBuf = path.into(); - let doc = HoconLoader::new() - .load_file(p) - .expect("Failed to load file") - .hocon() - .expect("Failed to load as HOCON"); + let doc = parse_config_file(p).expect("Failed to load as TOML"); Self::from_config(&doc) } @@ -385,8 +382,7 @@ impl fmt::Display for BufferError { #[cfg(test)] mod tests { use super::*; - use crate::prelude::*; - use hocon::HoconLoader; + use crate::{config::parse_config_str, prelude::*}; use std::time::Duration; const START_ACTOR_TIMEOUT: Duration = Duration::from_millis(1000); @@ -395,21 +391,16 @@ mod tests { #[ignore] #[should_panic(expected = "initial_chunk_count may not be greater than max_chunk_count")] fn invalid_pool_counts_config_validation() { - let hocon = HoconLoader::new() - .load_str( - r#"{ - buffer_config { - chunk_size: 64, - initial_chunk_count: 3, - max_chunk_count: 2, - encode_min_remaining: 2, - } - }"#, - ) - .unwrap() - .hocon(); - // This should succeed - let cfg = hocon.unwrap(); + let cfg = parse_config_str( + r#" + [buffer_config] + chunk_size = 64 + initial_chunk_count = 3 + max_chunk_count = 2 + encode_min_remaining = 2 + "#, + ) + .unwrap(); // Validation should panic let _ = BufferConfig::from_config(&cfg); @@ -501,14 +492,13 @@ mod tests { network_buffer_config.max_chunk_count(3); network_buffer_config.encode_buf_min_free_space(10); cfg.load_config_str( - r#"{ - buffer_config { - chunk_size: "256B", - initial_chunk_count: 3, - max_chunk_count: 4, - encode_min_remaining: "20B", - } - }"#, + r#" + [buffer_config] + chunk_size = "256B" + initial_chunk_count = 3 + max_chunk_count = 4 + encode_min_remaining = "20B" + "#, ); cfg.system_components(DeadletterBox::new, { NetworkConfig::with_buffer_config( @@ -519,11 +509,11 @@ mod tests { }); cfg.build().expect("KompactSystem") } - // This integration test sets up a KompactSystem with a Hocon BufferConfig, + // This integration test sets up a KompactSystem with a TOML BufferConfig, // then runs init_buffers on an Actor with different settings (in the on_start method of dummy), // and finally asserts that the actors buffers were set up using the on_start parameters. #[test] - fn buffer_config_init_buffers_overrides_hocon_and_default() { + fn buffer_config_init_buffers_overrides_toml_and_default() { let system = buffer_config_testing_system(); let mut dummy_actor = BufferTestActor::with_custom_buffer(); let future = dummy_actor.get_started_future(); @@ -552,7 +542,7 @@ mod tests { } #[test] - fn buffer_config_hocon_overrides_default() { + fn buffer_config_toml_overrides_default() { let system = buffer_config_testing_system(); let mut dummy_actor = BufferTestActor::without_custom_buffer(); let future = dummy_actor.get_started_future(); @@ -573,7 +563,7 @@ mod tests { buffer_write_pointer = encode_buffer.get_write_offset(); } }); - // Assert that the buffer was initialized with parameters in the hocon config string + // Assert that the buffer was initialized with parameters in the TOML config string assert_eq!(buffer_len, 256); // Check that the buffer was used assert_ne!(buffer_write_pointer, 0); diff --git a/core/src/runtime/config.rs b/core/src/runtime/config.rs index 8b10bb11..ac833a72 100644 --- a/core/src/runtime/config.rs +++ b/core/src/runtime/config.rs @@ -1,11 +1,10 @@ use super::*; use crate::{ - config::{ConfigEntry, ConfigError, ConfigValueType, HoconExt}, + config::{Config, ConfigEntry, ConfigError, ConfigLoadingError, ConfigValue, ConfigValueType}, messaging::DispatchEnvelope, }; use executors::*; -use hocon::Hocon; use std::{fmt, path::PathBuf, rc::Rc}; /// Configuration keys for Kompact systems. @@ -135,6 +134,24 @@ The default value is `auto`. pub(crate) enum ConfigSource { File(PathBuf), Str(String), + Value { + key: &'static str, + value: ConfigValue, + }, +} + +impl ConfigSource { + pub(crate) fn load(&self) -> Result { + match self { + ConfigSource::File(path) => crate::config::parse_config_file(path), + ConfigSource::Str(config) => crate::config::parse_config_str(config), + ConfigSource::Value { key, value } => { + let mut config = Config::new(); + config.insert_value(key, value.clone()); + Ok(config) + } + } + } } /// A configuration builder for Kompact systems @@ -198,11 +215,10 @@ impl fmt::Debug for KompactConfig { /// - It sets the event/message throughput to 2, split evenly between events and messages. /// - It runs with a single thread on a [small pool](crossbeam_workstealing_pool::small_pool). pub const MINIMAL_CONFIG: &str = r#" - kompact.system { - throughput = 2 - message-priority = 0.5 - threads = 1 - } +[kompact.runtime] +throughput = 2 +message-priority = 0.5 +threads = 1 "#; impl KompactConfig { @@ -461,13 +477,11 @@ impl KompactConfig { self } - /// Load a HOCON config from a file at `path` + /// Load a TOML config from a file at `path` /// /// This method can be called multiple times, and the resulting configurations will be merged. /// /// It matters in which order configs are loaded and values are set. - /// See [HoconLoader](hocon::HoconLoader) for more information. - /// /// The loaded config can be accessed via [`system.config()`](KompactSystem::config /// or from within a component via [`self.ctx().config()`](ComponentContext::config. pub fn load_config_file

(&mut self, path: P) -> &mut Self @@ -479,13 +493,11 @@ impl KompactConfig { self } - /// Load a HOCON config from a string + /// Load a TOML config from a string /// /// This method can be called multiple times, and the resulting configurations will be merged. /// /// It matters in which order configs are loaded and values are set. - /// See [HoconLoader](hocon::HoconLoader) for more information. - /// /// The loaded config can be accessed via [`system.config()`](KompactSystem::config /// or from within a component via [`self.ctx().config()`](ComponentContext::config. pub fn load_config_str(&mut self, config: S) -> &mut Self @@ -497,12 +509,11 @@ impl KompactConfig { self } - /// Override a single value in the HOCON config + /// Override a single value in the config /// /// This method can be called multiple times and the resulting configurations will be merged. /// /// It matters in which order configs are loaded and values are set. - /// See [HoconLoader](hocon::HoconLoader) for more information. pub fn set_config_value( &mut self, config: &ConfigEntry, @@ -511,11 +522,10 @@ impl KompactConfig { where T: ConfigValueType, { - let value_string = ::config_string(value); - self.config_sources.push(ConfigSource::Str(format!( - "{} = {}", - config.key, value_string - ))); + self.config_sources.push(ConfigSource::Value { + key: config.key, + value: ::into_config_value(value), + }); self } @@ -545,12 +555,12 @@ impl KompactConfig { mmf as usize } - pub(crate) fn override_from_hocon(&mut self, conf: &Hocon) -> Result<(), ConfigError> { - self.label = conf.get_or_default(&keys::LABEL)?; - self.throughput = conf.get_or_default(&keys::THROUGHPUT)?; - self.msg_priority = conf.get_or_default(&keys::MESSAGE_PRIORITY)?; - self.threads = conf.get_or_default(&keys::THREADS)?; - let scheduler_option = conf.get_or_default(&keys::SCHEDULER)?; + pub(crate) fn override_from_config(&mut self, conf: &Config) -> Result<(), ConfigError> { + self.label = conf.read_or_default(&keys::LABEL)?; + self.throughput = conf.read_or_default(&keys::THROUGHPUT)?; + self.msg_priority = conf.read_or_default(&keys::MESSAGE_PRIORITY)?; + self.threads = conf.read_or_default(&keys::THREADS)?; + let scheduler_option = conf.read_or_default(&keys::SCHEDULER)?; match scheduler_option.as_ref() { "auto" => { self.scheduler_builder = if self.threads <= 32 { @@ -601,7 +611,7 @@ impl Default for KompactConfig { /// It uses all default components, without networking, with the default timer and default logger. fn default() -> Self { // NOTE: Most of the values we are setting in here don't actually matter - // They will be overwritten by the default values of the config keys in `KompactConfig::override_from_hocon` + // They will be overwritten by the default values of the config keys in `KompactConfig::override_from_config` let scheduler_builder: Rc = Rc::new(|t| ExecutorScheduler::from(crossbeam_workstealing_pool::small_pool(t))); KompactConfig { diff --git a/core/src/runtime/mod.rs b/core/src/runtime/mod.rs index 20968195..10197ff6 100644 --- a/core/src/runtime/mod.rs +++ b/core/src/runtime/mod.rs @@ -11,7 +11,7 @@ use std::{ }, }; -use crate::config::ConfigError; +use crate::config::{ConfigError, ConfigLoadingError}; mod config; mod lifecycle; @@ -72,8 +72,8 @@ type TimerBuilder = dyn Fn() -> Box; pub enum KompactError { /// A mutex in the system has been poisoned Poisoned, - /// An error occurred loading the HOCON config - ConfigLoadingError(hocon::Error), + /// An error occurred loading the TOML config + ConfigLoadingError(ConfigLoadingError), /// An error occurred reading values from the loaded config ConfigError(ConfigError), /// Something else occurred @@ -94,17 +94,22 @@ impl PartialEq for KompactError { fn eq(&self, other: &Self) -> bool { match (self, other) { (KompactError::Poisoned, KompactError::Poisoned) => true, - (KompactError::ConfigLoadingError(she), KompactError::ConfigLoadingError(ohe)) => { - she == ohe - } + ( + KompactError::ConfigLoadingError(ConfigLoadingError::Io(lhs)), + KompactError::ConfigLoadingError(ConfigLoadingError::Io(rhs)), + ) => lhs.kind() == rhs.kind(), + ( + KompactError::ConfigLoadingError(ConfigLoadingError::Parse(lhs)), + KompactError::ConfigLoadingError(ConfigLoadingError::Parse(rhs)), + ) => lhs.to_string() == rhs.to_string(), (KompactError::ConfigError(se), KompactError::ConfigError(oe)) => se == oe, _ => false, } } } -impl From for KompactError { - fn from(e: hocon::Error) -> Self { +impl From for KompactError { + fn from(e: ConfigLoadingError) -> Self { KompactError::ConfigLoadingError(e) } } diff --git a/core/src/runtime/system.rs b/core/src/runtime/system.rs index cd876088..d02643b0 100644 --- a/core/src/runtime/system.rs +++ b/core/src/runtime/system.rs @@ -3,6 +3,7 @@ use super::*; #[cfg(feature = "type_erasure")] use crate::utils::erased::CreateErased; use crate::{ + config::Config, messaging::{ DispatchEnvelope, MsgEnvelope, @@ -16,7 +17,6 @@ use crate::{ supervision::{ComponentSupervisor, ListenEvent, SupervisionPort, SupervisorMsg}, timer::timer_manager::{CanCancelTimers, TimerRefFactory}, }; -use hocon::{Hocon, HoconLoader}; use oncemutex::{OnceMutex, OnceMutexGuard}; use std::{any::TypeId, fmt, sync::Mutex, time::Instant}; @@ -54,31 +54,24 @@ use std::{any::TypeId, fmt, sync::Mutex, time::Instant}; #[derive(Clone)] pub struct KompactSystem { inner: Arc, - config: Arc, + config: Arc, scheduler: Box, } impl KompactSystem { - fn load_config(conf: &KompactConfig) -> Result { - let config_loader_initial: Result = - Result::Ok(HoconLoader::new()); - let config = conf - .config_sources - .iter() - .fold(config_loader_initial, |config_loader, source| { - config_loader.and_then(|cl| match source { - ConfigSource::File(path) => cl.load_file(path), - ConfigSource::Str(s) => cl.load_str(s), - }) - })? - .hocon()?; + fn load_config(conf: &KompactConfig) -> Result { + let mut config = Config::new(); + for source in &conf.config_sources { + let next = source.load()?; + config.merge(next); + } Ok(config) } /// Use the [build](KompactConfig::build) method instead. pub(crate) fn try_new(mut conf: KompactConfig) -> Result { let config = Self::load_config(&conf)?; - conf.override_from_hocon(&config)?; + conf.override_from_config(&config)?; let scheduler = (*conf.scheduler_builder)(conf.threads); let sc_builder = conf.sc_builder.clone(); @@ -149,25 +142,25 @@ impl KompactSystem { /// Get a reference to the system configuration /// /// Use [load_config_str](KompactConfig::load_config_str) or - /// or [load_config_file](KompactConfig::load_config_file) + /// [load_config_file](KompactConfig::load_config_file) /// to load values into the config object. /// /// # Example /// /// ``` /// use kompact::prelude::*; - /// let default_values = r#"{ a = 7 }"#; + /// let default_values = r#"a = 7"#; /// let mut conf = KompactConfig::default(); /// conf.load_config_str(default_values); /// let system = conf.build().expect("system"); /// assert_eq!(Some(7i64), system.config()["a"].as_i64()); /// ``` - pub fn config(&self) -> &Hocon { + pub fn config(&self) -> &Config { self.config.as_ref() } /// Get a owned reference to the system configuration - pub fn config_owned(&self) -> Arc { + pub fn config_owned(&self) -> Arc { self.config.clone() } diff --git a/docs/examples/app_settings.toml b/docs/examples/app_settings.toml new file mode 100644 index 00000000..f948ebdf --- /dev/null +++ b/docs/examples/app_settings.toml @@ -0,0 +1,7 @@ +[buncher] +batch-size = 100 +timeout = "100 ms" + +[omega] +initial-period = "10 ms" +delta = "1 ms" diff --git a/docs/examples/application.conf b/docs/examples/application.conf deleted file mode 100644 index 80279e09..00000000 --- a/docs/examples/application.conf +++ /dev/null @@ -1,8 +0,0 @@ -buncher { - batch-size = 100 - timeout = 100 ms -} -omega { - initial-period = 10 ms - delta = 1 ms -} diff --git a/docs/examples/src/bin/bootstrapping.rs b/docs/examples/src/bin/bootstrapping.rs index ab0f7753..3c06ec2c 100644 --- a/docs/examples/src/bin/bootstrapping.rs +++ b/docs/examples/src/bin/bootstrapping.rs @@ -231,7 +231,7 @@ const BOOTSTRAP_PATH: &str = "bootstrap"; pub fn run_server(socket: SocketAddr) -> KompactSystem { let mut cfg = KompactConfig::default(); - cfg.load_config_file("./application.conf"); + cfg.load_config_file("./app_settings.toml"); cfg.system_components(DeadletterBox::new, NetworkConfig::new(socket).build()); let system = cfg.build().expect("KompactSystem"); @@ -260,7 +260,7 @@ pub fn run_server(socket: SocketAddr) -> KompactSystem { // ANCHOR: client pub fn run_client(bootstrap_socket: SocketAddr, client_socket: SocketAddr) -> KompactSystem { let mut cfg = KompactConfig::default(); - cfg.load_config_file("./application.conf"); + cfg.load_config_file("./app_settings.toml"); cfg.system_components( DeadletterBox::new, NetworkConfig::new(client_socket).build(), diff --git a/docs/examples/src/bin/buncher_config.rs b/docs/examples/src/bin/buncher_config.rs index ab6972f5..d6b20516 100644 --- a/docs/examples/src/bin/buncher_config.rs +++ b/docs/examples/src/bin/buncher_config.rs @@ -99,7 +99,7 @@ pub fn main() { // ANCHOR: system let mut conf = KompactConfig::default(); // ANCHOR: config_file - conf.load_config_file("./application.conf") + conf.load_config_file("./app_settings.toml") // ANCHOR_END: config_file .load_config_str("buncher.batch-size = 50"); let system = conf.build().expect("system"); diff --git a/docs/examples/src/bin/leader_election.rs b/docs/examples/src/bin/leader_election.rs index 2c5a5167..080ab5b2 100644 --- a/docs/examples/src/bin/leader_election.rs +++ b/docs/examples/src/bin/leader_election.rs @@ -158,7 +158,7 @@ pub fn run_systems(num_systems: usize) { let mut systems: Vec = { let system = || { let mut cfg = KompactConfig::default(); - cfg.load_config_file("./application.conf"); + cfg.load_config_file("./app_settings.toml"); cfg.system_components(DeadletterBox::new, NetworkConfig::default().build()); cfg.build().expect("KompactSystem") }; diff --git a/docs/examples/src/bin/serialisation.rs b/docs/examples/src/bin/serialisation.rs index 05a570f2..ff90730c 100644 --- a/docs/examples/src/bin/serialisation.rs +++ b/docs/examples/src/bin/serialisation.rs @@ -315,7 +315,7 @@ const BOOTSTRAP_PATH: &str = "bootstrap"; pub fn run_server(socket: SocketAddr) -> KompactSystem { let mut cfg = KompactConfig::default(); - cfg.load_config_file("./application.conf"); + cfg.load_config_file("./app_settings.toml"); cfg.system_components(DeadletterBox::new, NetworkConfig::new(socket).build()); let system = cfg.build().expect("KompactSystem"); @@ -342,7 +342,7 @@ pub fn run_server(socket: SocketAddr) -> KompactSystem { pub fn run_client(bootstrap_socket: SocketAddr, client_socket: SocketAddr) -> KompactSystem { let mut cfg = KompactConfig::default(); - cfg.load_config_file("./application.conf"); + cfg.load_config_file("./app_settings.toml"); cfg.system_components( DeadletterBox::new, NetworkConfig::new(client_socket).build(), diff --git a/docs/src/distributed/basiccommunication.md b/docs/src/distributed/basiccommunication.md index b7a546f2..96a5da77 100644 --- a/docs/src/distributed/basiccommunication.md +++ b/docs/src/distributed/basiccommunication.md @@ -54,13 +54,12 @@ There is a bit of state we need to keep track of in our `EventualLeaderElector` {{#rustdoc_include ../../examples/src/bin/leader_election.rs:state}} ``` -In order to load our configuration values from a file, we need to put something like the following into an `application.conf` file in the current working directory: +In order to load our configuration values from a file, we need to put something like the following into an `app_settings.toml` file in the current working directory: -```hocon -omega { - initial-period = 10 ms - delta = 1 ms -} +```toml +[omega] +initial-period = "10 ms" +delta = "1 ms" ``` And then we can load it and start the initial timeout in the `on_start` handler as before: diff --git a/docs/src/distributed/networkbuffers.md b/docs/src/distributed/networkbuffers.md index 99cd0226..7d1a4738 100644 --- a/docs/src/distributed/networkbuffers.md +++ b/docs/src/distributed/networkbuffers.md @@ -64,18 +64,17 @@ impl ComponentLifecycle for CustomBufferConfigActor { ``` #### Configuring All Actors -If a programmer wishes for all actors to use the same `BufferConfig` configuration, a Hocon string can be inserted into the `KompactConfig` or loaded from a Hocon-file ([see configuration chapter on loading configurations](./../local/configuration.md)), for example: +If a programmer wishes for all actors to use the same `BufferConfig` configuration, a TOML string can be inserted into the `KompactConfig` or loaded from a TOML file ([see configuration chapter on loading configurations](./../local/configuration.md)), for example: ```rust,edition2018,no_run,noplaypen let mut cfg = KompactConfig::new(); cfg.load_config_str( - r#"{ - buffer_config { - chunk_size: "256KB", - initial_chunk_count: 3, - max_chunk_count: 4, - encode_min_remaining: "20B", - } - }"#, + r#" + [buffer_config] + chunk_size = "256KB" + initial_chunk_count = 3 + max_chunk_count = 4 + encode_min_remaining = "20B" + "#, ); ... let system = cfg.build().expect("KompactSystem"); diff --git a/docs/src/local/configuration.md b/docs/src/local/configuration.md index c9fee6a2..ef0e0eef 100644 --- a/docs/src/local/configuration.md +++ b/docs/src/local/configuration.md @@ -1,10 +1,10 @@ # Configuration -Since it is often inconvenient to pass around a large number of parameters when setting up a component system, Kompact also offers a configuration system allowing parameters to be loaded from a file or provided as a large string at the top level, for example. This system is powered by the [Hocon](https://crates.io/crates/hocon) crate and uses its APIs with very little additional support. +Since it is often inconvenient to pass around a large number of parameters when setting up a component system, Kompact also offers a configuration system allowing parameters to be loaded from a file or provided as a large string at the top level, for example. The configuration format is TOML and the parsed values can be accessed directly via the Kompact config API. Configuration options must be set on the `KompactConfig` instance before the system is started and the resulting configuration remains immutable for the lifetime of the system. A configuration can be loaded from a file by passing a path to the file to the `load_config_file(...)` function. Alternatively, configuration values can be loaded directly from a string using `load_config_str(...)`. -Within each component the [Hocon](https://docs.rs/hocon/latest/hocon/enum.Hocon.html) configuration instance can be accessed via the context and individual keys via bracket notation, e.g. `self.ctx.config()["my-key"]`. The configuration can also be accessed outside a component via `KompactSystem::config()`. +Within each component the configuration instance can be accessed via the context and individual keys via bracket notation, e.g. `self.ctx.config()["my-key"]`. This panics on invalid access, just like normal Rust indexing. For fallible path-aware access with detailed errors, use `self.ctx.config().get("my-key")` or `self.ctx.config().select("my-group.my-key")` instead. The configuration can also be accessed outside a component via `KompactSystem::config()`. In addition to component configuration, many parts of Kompact's runtime can also be configured via this mechanism. The complete set of available configuration keys and their effects is described in the modules below [kompact::config_keys](https://docs.rs/kompact/latest/kompact/config_keys/index.html). @@ -12,13 +12,13 @@ In addition to component configuration, many parts of Kompact's runtime can also We are going to reuse the `Buncher` from the [timers](timers.md) section and pass its two parameters, `batch_size` and `timeout`, via configuration instead of the constructor. -We'll start off by creating a configuration file `application.conf` in the working directory, so its easy to find later. Something like this: +We'll start off by creating a configuration file `app_settings.toml` in the working directory, so its easy to find later. Something like this: -```hocon -{{#rustdoc_include ../../examples/application.conf}} +```toml +{{#rustdoc_include ../../examples/app_settings.toml}} ``` -We can then add this file to the `KompicsConfig` instance using the `load_config_file(...)` function: +We can then add this file to the `KompactConfig` instance using the `load_config_file(...)` function: ```rust,edition2018,no_run,noplaypen {{#rustdoc_include ../../examples/src/bin/buncher_config.rs:config_file}} @@ -42,7 +42,7 @@ And, of course, we must also update the matching `create(...)` call in the main {{#rustdoc_include ../../examples/src/bin/buncher_config.rs:create_buncher}} ``` -Finally, the actual config access happens in the `on_start`code. At this point the component is properly initialised and we have acceess to configuration values. The [Hocon](https://docs.rs/hocon/latest/hocon/enum.Hocon.html) type has a bunch of very convenient conversion functions, so we can get a `Duration` directly from the `100 ms` string in the file, for example. Once we have read the values for `batch_size` and `timeout`, we can also go ahead and reserve the required additional space in the `current_batch` vector. +Finally, the actual config access happens in the `on_start` code. At this point the component is properly initialised and we have access to configuration values. The Kompact config type has convenient conversion functions, so we can get a `Duration` directly from the `"100 ms"` string in the file, for example. Once we have read the values for `batch_size` and `timeout`, we can also go ahead and reserve the required additional space in the `current_batch` vector. ```rust,edition2018,no_run,noplaypen {{#rustdoc_include ../../examples/src/bin/buncher_config.rs:on_start}}