From 9d91c77c10e38b070a7ea241afb3a5ea169fcbc0 Mon Sep 17 00:00:00 2001 From: 0x009922 <43530070+0x009922@users.noreply.github.com> Date: Fri, 17 May 2024 19:42:39 +0900 Subject: [PATCH] feat: improve config debug-ability (#4456) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [refactor]: put new API into `iroha_config_base` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [test]: move tests Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [feat]: create foundation for `ReadConfig` derive (wip) Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: parse `default` properly Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [feat]: impl shape analysis Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [test]: update stderr Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: fix macro, it kind of works! Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: improve errors Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: update `iroha_config` (wip) Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: update user layer, mostly Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: move `iroha_config` onto the new rails Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: lints & chores Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: fix visibility issues in data model Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: after-rebase chores Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: update `iroha_core` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: update the whole workspace Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: lints Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [chore]: fix cfg Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: balance trace logs Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: move high-level validations to CLI layer Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: change `custom` to `env_custom` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [feat]: attach TOML value to report Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [feat]: enhance origin attachments Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: client cli... works? Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: rehearse errors Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: chores Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: compiler errors and lints Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [test]: fix tests, validate addrs only in `release` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: internal docs, renamings, lints Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [test]: fix private keys in test configs Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: allow self peer id in trusted peers Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [test]: update snapshot Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: ⭐️NINIKA⭐️ Signed-off-by: 0x009922 <43530070+0x009922@users.noreply.github.com> * [feat]: prefix macro error Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [docs]: add a note about syntactic match Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: delegate parsing to `syn::punctuated` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: use mutable reader, split `ReadingDone` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: lints Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [chore]: link false-positive issue https://github.com/rust-lang/rust/issues/44752#issuecomment-1712086069 Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: ignore report locations address https://github.com/hyperledger/iroha/pull/4456#issuecomment-2066327874 Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: use `ExposedPrivateKey` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [fix]: fix with all-features Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [refactor]: chores Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * [chore]: format Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * refactor: use stricter `TrustedPeers` struct Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * chore: remove extra TODO Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * refactor: remove `env_custom` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * chore: remove extra import Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * revert: return `pub(crate)` vis Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * chore: dead import Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> * fix: fix path to `iroha_test_config.toml` Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> --------- Signed-off-by: Dmitry Balashov <43530070+0x009922@users.noreply.github.com> Signed-off-by: 0x009922 <43530070+0x009922@users.noreply.github.com> Co-authored-by: ⭐️NINIKA⭐️ --- Cargo.lock | 845 +++++++--------- Cargo.toml | 3 + cli/Cargo.toml | 7 +- cli/src/lib.rs | 473 ++++++--- cli/src/main.rs | 63 +- cli/src/samples.rs | 122 +-- client/Cargo.toml | 6 +- client/benches/torii.rs | 16 +- client/benches/tps/utils.rs | 2 +- client/examples/million_accounts_genesis.rs | 8 +- client/examples/register_1000_triggers.rs | 8 +- client/src/config.rs | 34 +- client/src/config/user.rs | 129 ++- client/src/config/user/boilerplate.rs | 147 --- client/tests/integration/events/pipeline.rs | 2 +- .../extra_functional/connected_peers.rs | 2 +- .../integration/triggers/time_trigger.rs | 2 +- client_cli/Cargo.toml | 6 +- client_cli/src/main.rs | 47 +- config/Cargo.toml | 15 +- config/base/Cargo.toml | 20 +- config/base/derive/Cargo.toml | 28 + config/base/derive/src/lib.rs | 641 +++++++++++++ config/base/derive/tests/ui.rs | 9 + config/base/derive/tests/ui_fail/generics.rs | 8 + .../base/derive/tests/ui_fail/generics.stderr | 5 + .../tests/ui_fail/invalid_attrs_commas.rs | 9 + .../tests/ui_fail/invalid_attrs_commas.stderr | 5 + .../tests/ui_fail/invalid_attrs_conflicts.rs | 15 + .../ui_fail/invalid_attrs_conflicts.stderr | 11 + .../invalid_attrs_default_invalid_expr.rs | 9 + .../invalid_attrs_default_invalid_expr.stderr | 5 + .../ui_fail/invalid_attrs_env_without_var.rs | 9 + .../invalid_attrs_env_without_var.stderr | 5 + .../invalid_attrs_no_comma_between_attrs.rs | 9 + ...nvalid_attrs_no_comma_between_attrs.stderr | 5 + .../tests/ui_fail/invalid_attrs_struct.rs | 9 + .../tests/ui_fail/invalid_attrs_struct.stderr | 5 + .../tests/ui_fail/unsupported_shapes.rs | 17 + .../tests/ui_fail/unsupported_shapes.stderr | 31 + .../base/derive/tests/ui_pass/happy_path.rs | 26 + config/base/src/attach.rs | 233 +++++ config/base/src/env.rs | 138 +++ config/base/src/lib.rs | 901 +++++------------- config/base/src/read.rs | 596 ++++++++++++ config/base/src/toml.rs | 428 +++++++++ config/base/src/util.rs | 255 +++++ config/base/tests/bad.invalid-extends.toml | 1 + .../bad.invalid-nested-extends.base.toml | 1 + .../tests/bad.invalid-nested-extends.toml | 1 + config/base/tests/misc.rs | 488 ++++++++++ config/src/lib.rs | 12 + config/src/parameters/actual.rs | 120 ++- config/src/parameters/defaults.rs | 62 +- config/src/parameters/user.rs | 635 ++++++------ config/src/parameters/user/boilerplate.rs | 777 --------------- config/tests/fixtures.rs | 475 +++------ .../tests/fixtures/bad.extends_nowhere.toml | 1 - config/tests/fixtures/full.env | 1 + config/tests/fixtures/full.toml | 2 +- config/tests/fixtures/multiple_extends.1.toml | 2 - config/tests/fixtures/multiple_extends.2.toml | 5 - .../tests/fixtures/multiple_extends.2a.toml | 2 - config/tests/fixtures/multiple_extends.toml | 6 - configs/peer.template.toml | 2 +- core/benches/kura.rs | 4 +- core/src/block.rs | 2 +- core/src/kiso.rs | 20 +- core/src/kura.rs | 9 +- core/src/queue.rs | 20 +- core/src/smartcontracts/isi/world.rs | 6 +- core/src/snapshot.rs | 9 +- core/src/state.rs | 13 +- core/src/sumeragi/mod.rs | 11 +- core/test_network/src/lib.rs | 61 +- p2p/src/network.rs | 6 +- p2p/tests/integration/p2p.rs | 9 +- primitives/src/unique_vec.rs | 21 +- tools/kagami/src/genesis.rs | 52 +- tools/swarm/src/compose.rs | 22 +- torii/src/lib.rs | 35 +- 81 files changed, 4916 insertions(+), 3346 deletions(-) create mode 100644 config/base/derive/Cargo.toml create mode 100644 config/base/derive/src/lib.rs create mode 100644 config/base/derive/tests/ui.rs create mode 100644 config/base/derive/tests/ui_fail/generics.rs create mode 100644 config/base/derive/tests/ui_fail/generics.stderr create mode 100644 config/base/derive/tests/ui_fail/invalid_attrs_commas.rs create mode 100644 config/base/derive/tests/ui_fail/invalid_attrs_commas.stderr create mode 100644 config/base/derive/tests/ui_fail/invalid_attrs_conflicts.rs create mode 100644 config/base/derive/tests/ui_fail/invalid_attrs_conflicts.stderr create mode 100644 config/base/derive/tests/ui_fail/invalid_attrs_default_invalid_expr.rs create mode 100644 config/base/derive/tests/ui_fail/invalid_attrs_default_invalid_expr.stderr create mode 100644 config/base/derive/tests/ui_fail/invalid_attrs_env_without_var.rs create mode 100644 config/base/derive/tests/ui_fail/invalid_attrs_env_without_var.stderr create mode 100644 config/base/derive/tests/ui_fail/invalid_attrs_no_comma_between_attrs.rs create mode 100644 config/base/derive/tests/ui_fail/invalid_attrs_no_comma_between_attrs.stderr create mode 100644 config/base/derive/tests/ui_fail/invalid_attrs_struct.rs create mode 100644 config/base/derive/tests/ui_fail/invalid_attrs_struct.stderr create mode 100644 config/base/derive/tests/ui_fail/unsupported_shapes.rs create mode 100644 config/base/derive/tests/ui_fail/unsupported_shapes.stderr create mode 100644 config/base/derive/tests/ui_pass/happy_path.rs create mode 100644 config/base/src/attach.rs create mode 100644 config/base/src/env.rs create mode 100644 config/base/src/read.rs create mode 100644 config/base/src/toml.rs create mode 100644 config/base/src/util.rs create mode 100644 config/base/tests/bad.invalid-extends.toml create mode 100644 config/base/tests/bad.invalid-nested-extends.base.toml create mode 100644 config/base/tests/bad.invalid-nested-extends.toml create mode 100644 config/base/tests/misc.rs delete mode 100644 config/tests/fixtures/bad.extends_nowhere.toml delete mode 100644 config/tests/fixtures/multiple_extends.1.toml delete mode 100644 config/tests/fixtures/multiple_extends.2.toml delete mode 100644 config/tests/fixtures/multiple_extends.2a.toml delete mode 100644 config/tests/fixtures/multiple_extends.toml diff --git a/Cargo.lock b/Cargo.lock index 8679aae63a1..5aa9b152253 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,47 +101,48 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -164,9 +165,9 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] name = "ark-bls12-377" @@ -376,9 +377,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" @@ -427,9 +428,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -473,21 +474,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - [[package]] name = "bitflags" version = "1.3.2" @@ -532,9 +518,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0901fc8eb0aca4c83be0106d6f2db17d86a08dfc2c25f0e84464bf381158add6" +checksum = "dbe5b10e214954177fb1dc9fbd20a1a2608fe99e6c832033bdc7cea287a20d77" dependencies = [ "borsh-derive", "cfg_aliases", @@ -542,9 +528,9 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51670c3aa053938b0ee3bd67c3817e471e626151131b934038e83c5bf8de48f5" +checksum = "d7a8646f94ab393e43e8b35a2558b1624bed28b97ee09c5d15456e3c9463f46d" dependencies = [ "once_cell", "proc-macro-crate", @@ -556,9 +542,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.8.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" +checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" dependencies = [ "memchr", "regex-automata 0.4.6", @@ -576,9 +562,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byte-slice-cast" @@ -633,18 +619,18 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "camino" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" dependencies = [ "serde", ] [[package]] name = "cargo-platform" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "694c8807f2ae16faecc43dc17d74b3eb042482789fd0eb64b39a2e04e087053f" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" dependencies = [ "serde", ] @@ -671,12 +657,13 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.94" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" +checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" dependencies = [ "jobserver", "libc", + "once_cell", ] [[package]] @@ -727,14 +714,14 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.0", + "windows-targets 0.52.5", ] [[package]] name = "ciborium" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", @@ -743,15 +730,15 @@ dependencies = [ [[package]] name = "ciborium-io" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", "half", @@ -810,9 +797,9 @@ checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "clru" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8191fa7302e03607ff0e237d4246cc043ff5b3cb9409d995172ba3bea16b807" +checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59" [[package]] name = "codespan-reporting" @@ -853,9 +840,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "colored" @@ -885,19 +872,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "console" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" -dependencies = [ - "encode_unicode", - "lazy_static", - "libc", - "unicode-width", - "windows-sys 0.45.0", -] - [[package]] name = "console-api" version = "0.6.0" @@ -1022,7 +996,7 @@ dependencies = [ "cranelift-entity", "cranelift-isle", "gimli", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "log", "regalloc2", "smallvec", @@ -1155,11 +1129,10 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.9" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c3242926edf34aec4ac3a77108ad4854bffaa2e4ddc1824124ce59231302d5" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" dependencies = [ - "cfg-if", "crossbeam-utils", ] @@ -1222,6 +1195,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -1274,9 +1253,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.110" +version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7129e341034ecb940c9072817cd9007974ea696844fc4dd582dc1653a7fbe2e8" +checksum = "bb497fad022245b29c2a0351df572e2d67c1046bcef2260ebc022aec81efea82" dependencies = [ "cc", "cxxbridge-flags", @@ -1286,9 +1265,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.110" +version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a24f3f5f8eed71936f21e570436f024f5c2e25628f7496aa7ccd03b90109d5" +checksum = "9327c7f9fbd6329a200a5d4aa6f674c60ab256525ff0084b52a889d4e4c60cee" dependencies = [ "cc", "codespan-reporting", @@ -1301,15 +1280,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.110" +version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06fdd177fc61050d63f67f5bd6351fac6ab5526694ea8e359cd9cd3b75857f44" +checksum = "688c799a4a846f1c0acb9f36bb9c6272d9b3d9457f3633c7753c6057270df13c" [[package]] name = "cxxbridge-macro" -version = "1.0.110" +version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "587663dd5fb3d10932c8aecfe7c844db1bcf0aee93eeab08fac13dc1212c2e7f" +checksum = "928bc249a7e3cd554fd2e8e08a426e9670c50bbfc9a621653cfa9accc9641783" dependencies = [ "proc-macro2", "quote", @@ -1364,7 +1343,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -1372,9 +1351,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "debugid" @@ -1387,9 +1366,9 @@ dependencies = [ [[package]] name = "der" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", "zeroize", @@ -1438,17 +1417,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "dialoguer" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" -dependencies = [ - "console", - "shell-words", - "thiserror", -] - [[package]] name = "digest" version = "0.10.7" @@ -1495,9 +1463,9 @@ dependencies = [ [[package]] name = "dissimilar" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86e3bdc80eee6e16b2b6b0f87fbc98c04bee3455e35174c0de1a125d0688c632" +checksum = "59f8e79d1fbf76bdfbde321e902714bf6c49df88a7dda6fc682fc2979226962d" [[package]] name = "drop_bomb" @@ -1513,9 +1481,9 @@ checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" [[package]] name = "dyn-clone" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "ecdsa" @@ -1581,17 +1549,11 @@ dependencies = [ "zeroize", ] -[[package]] -name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] @@ -1614,14 +1576,25 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", ] +[[package]] +name = "error-stack" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27a72baa257b5e0e2de241967bc5ee8f855d6072351042688621081d66b2a76b" +dependencies = [ + "anyhow", + "eyre", + "rustc_version", +] + [[package]] name = "expect-test" version = "1.5.0" @@ -1656,9 +1629,9 @@ checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183" [[package]] name = "fastrand" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "ff" @@ -1672,9 +1645,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.5" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" @@ -1684,7 +1657,7 @@ checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "windows-sys 0.52.0", ] @@ -1708,9 +1681,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -2033,9 +2006,9 @@ dependencies = [ [[package]] name = "gix-date" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180b130a4a41870edfbd36ce4169c7090bca70e195da783dea088dd973daa59c" +checksum = "367ee9093b0c2b04fd04c5c7c8b6a1082713534eab537597ae343663a518fa99" dependencies = [ "bstr", "itoa", @@ -2126,7 +2099,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ddf80e16f3c19ac06ce415a38b8591993d3f73aede049cb561becb5b3a8e242" dependencies = [ "gix-hash", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "parking_lot", ] @@ -2427,9 +2400,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.24" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -2446,9 +2419,13 @@ dependencies = [ [[package]] name = "half" -version = "1.8.2" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] [[package]] name = "hashbrown" @@ -2470,9 +2447,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash 0.8.11", "allocator-api2", @@ -2538,9 +2515,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -2577,11 +2554,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.5" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -2673,9 +2650,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2757,7 +2734,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "serde", ] @@ -2792,7 +2769,7 @@ version = "2.0.0-pre-rc.21" dependencies = [ "assertables", "clap", - "color-eyre", + "error-stack", "eyre", "futures", "iroha_config", @@ -2814,6 +2791,7 @@ dependencies = [ "serial_test", "supports-color 2.1.0", "tempfile", + "thiserror", "thread-local-panic-hook", "tokio", "toml 0.8.13", @@ -2831,12 +2809,14 @@ dependencies = [ "criterion", "derive_more", "displaydoc", + "error-stack", "eyre", "futures-util", "hex", "http 1.1.0", "iroha", "iroha_config", + "iroha_config_base", "iroha_crypto", "iroha_data_model", "iroha_genesis", @@ -2846,7 +2826,6 @@ dependencies = [ "iroha_torii_const", "iroha_version", "iroha_wasm_builder", - "merge", "nonzero_ext", "once_cell", "parity-scale-codec", @@ -2873,14 +2852,17 @@ version = "2.0.0-pre-rc.21" dependencies = [ "clap", "color-eyre", - "dialoguer", "erased-serde", + "error-stack", + "eyre", "iroha_client", "iroha_config_base", "iroha_primitives", "json5", "once_cell", "serde_json", + "supports-color 2.1.0", + "thiserror", "vergen", ] @@ -2888,11 +2870,12 @@ dependencies = [ name = "iroha_config" version = "2.0.0-pre-rc.21" dependencies = [ + "assertables", "cfg-if", "derive_more", "displaydoc", + "error-stack", "expect-test", - "eyre", "hex", "iroha_config_base", "iroha_crypto", @@ -2900,20 +2883,16 @@ dependencies = [ "iroha_genesis", "iroha_primitives", "json5", - "merge", "nonzero_ext", "once_cell", - "proptest", "serde", "serde_json", "serde_with", - "stacker", + "stderrlog", "strum 0.25.0", "thiserror", - "toml 0.8.13", "tracing", "tracing-subscriber", - "trybuild", "url", ] @@ -2923,16 +2902,35 @@ version = "2.0.0-pre-rc.21" dependencies = [ "derive_more", "drop_bomb", - "eyre", - "merge", + "error-stack", + "expect-test", + "hex", + "iroha_config_base_derive", + "log", "num-traits", "serde", - "serde_json", "serde_with", + "strum 0.25.0", "thiserror", "toml 0.8.13", ] +[[package]] +name = "iroha_config_base_derive" +version = "2.0.0-pre-rc.21" +dependencies = [ + "darling", + "expect-test", + "iroha_config_base", + "iroha_macro_utils", + "manyhow", + "proc-macro2", + "quote", + "serde", + "syn 2.0.63", + "trybuild", +] + [[package]] name = "iroha_core" version = "2.0.0-pre-rc.21" @@ -3569,20 +3567,26 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ - "hermit-abi 0.3.3", - "rustix", - "windows-sys 0.48.0", + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.52.0", ] [[package]] name = "is_ci" -version = "1.1.1" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" [[package]] name = "itertools" @@ -3595,9 +3599,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] @@ -3630,18 +3634,18 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.27" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.66" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -3691,9 +3695,9 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ "cpufeatures", ] @@ -3746,25 +3750,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e0d73b369f386f1c44abd9c570d5318f55ccde816ff4b562fa452e5182863d" dependencies = [ "core2", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "rle-decode-fast", ] -[[package]] -name = "libm" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" - [[package]] name = "libredox" -version = "0.0.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.5.0", "libc", - "redox_syscall", ] [[package]] @@ -3795,15 +3792,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -3821,7 +3818,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -3889,9 +3886,9 @@ dependencies = [ [[package]] name = "memmap2" -version = "0.9.0" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deaba38d7abf1d4cca21cc89e932e542ba2b9258664d2a9ef0e61512039c9375" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" dependencies = [ "libc", ] @@ -3905,28 +3902,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "merge" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10bbef93abb1da61525bbc45eeaff6473a41907d19f8f9aa5168d214e10693e9" -dependencies = [ - "merge_derive", - "num-traits", -] - -[[package]] -name = "merge_derive" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209d075476da2e63b4b29e72a2ef627b840589588e71400a25e3565c4f849d07" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "mime" version = "0.3.17" @@ -4060,11 +4035,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -4077,11 +4051,10 @@ checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] @@ -4092,7 +4065,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -4101,15 +4073,15 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.3", + "hermit-abi 0.3.9", "libc", ] [[package]] name = "num_threads" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ "libc", ] @@ -4121,7 +4093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "crc32fast", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "indexmap 2.2.6", "memchr", ] @@ -4140,9 +4112,9 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "opaque-debug" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" @@ -4178,9 +4150,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.2.1+3.2.0" +version = "300.2.3+3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fe476c29791a5ca0d1273c697e96085bbabbbea2ef7afd5617e78a4b40332d3" +checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" dependencies = [ "cc", ] @@ -4271,25 +4243,25 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "backtrace", "cfg-if", "libc", "petgraph", - "redox_syscall", + "redox_syscall 0.5.1", "smallvec", "thread-id", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "path-absolutize" @@ -4323,9 +4295,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.5" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5" +checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" dependencies = [ "memchr", "thiserror", @@ -4334,9 +4306,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.5" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81d78524685f5ef2a3b3bd1cafbc9fcabb036253d9b1463e726a91cd16e2dfc2" +checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" dependencies = [ "pest", "pest_generator", @@ -4344,9 +4316,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.5" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68bd1206e71118b5356dae5ddc61c8b11e28b09ef6a31acbd15ea48a28e0c227" +checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" dependencies = [ "pest", "pest_meta", @@ -4357,9 +4329,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.5" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c747191d4ad9e4a4ab9c8798f1e82a39affe7ef9648390b7e5548d18e099de6" +checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" dependencies = [ "once_cell", "pest", @@ -4368,9 +4340,9 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", "indexmap 2.2.6", @@ -4378,18 +4350,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", @@ -4575,31 +4547,11 @@ dependencies = [ "thiserror", ] -[[package]] -name = "proptest" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" -dependencies = [ - "bit-set", - "bit-vec", - "bitflags 2.5.0", - "lazy_static", - "num-traits", - "rand", - "rand_chacha", - "rand_xorshift", - "regex-syntax 0.8.3", - "rusty-fork", - "tempfile", - "unarray", -] - [[package]] name = "prost" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +checksum = "d0f5d036824e4761737860779c906171497f6d55681139d8312388f8fe398922" dependencies = [ "bytes", "prost-derive", @@ -4607,12 +4559,12 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +checksum = "9554e3ab233f0a932403704f1a1d08c30d5ccd931adfdfa1e8b5a19b52c1d55a" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.12.1", "proc-macro2", "quote", "syn 2.0.63", @@ -4620,9 +4572,9 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +checksum = "3235c33eb02c1f1e212abdbe34c78b264b038fb58ca612664343271e36e55ffe" dependencies = [ "prost", ] @@ -4681,12 +4633,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - [[package]] name = "quote" version = "1.0.36" @@ -4732,15 +4678,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "rand_xorshift" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" -dependencies = [ - "rand_core", -] - [[package]] name = "rayon" version = "1.10.0" @@ -4770,11 +4707,20 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +dependencies = [ + "bitflags 2.5.0", +] + [[package]] name = "redox_users" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom", "libredox", @@ -4859,16 +4805,17 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.7" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", + "cfg-if", "getrandom", "libc", "spin", "untrusted", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -4924,9 +4871,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -4945,9 +4892,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.32" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.5.0", "errno", @@ -4995,15 +4942,15 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.4.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" -version = "0.102.2" +version = "0.102.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" dependencies = [ "ring", "rustls-pki-types", @@ -5012,27 +4959,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" - -[[package]] -name = "rusty-fork" -version = "0.3.0" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" -dependencies = [ - "fnv", - "quick-error", - "tempfile", - "wait-timeout", -] +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -5045,20 +4980,20 @@ dependencies = [ [[package]] name = "scc" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec96560eea317a9cc4e0bb1f6a2c93c09a19b8c4fc5cb3fcc0ec1c094cd783e2" +checksum = "76ad2bbb0ae5100a07b7a6f2ed7ab5fd0045551a4c507989b7a620046ea3efdc" dependencies = [ "sdd", ] [[package]] name = "schannel" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -5127,11 +5062,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.9.2" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "core-foundation", "core-foundation-sys", "libc", @@ -5140,9 +5075,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" dependencies = [ "core-foundation-sys", "libc", @@ -5150,9 +5085,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" dependencies = [ "serde", ] @@ -5337,12 +5272,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shell-words" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" - [[package]] name = "signal-hook" version = "0.3.17" @@ -5366,9 +5295,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -5425,9 +5354,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", @@ -5473,16 +5402,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] -name = "stacker" -version = "0.1.15" +name = "stderrlog" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" +checksum = "61c910772f992ab17d32d6760e167d2353f4130ed50e796752689556af07dc6b" dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "winapi", + "chrono", + "is-terminal", + "log", + "termcolor", + "thread_local", ] [[package]] @@ -5676,9 +5605,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.4.1" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" dependencies = [ "winapi-util", ] @@ -5756,9 +5685,9 @@ checksum = "e70399498abd3ec85f99a2f2d765c8638588e20361678af93a9f47de96719743" [[package]] name = "thread_local" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", @@ -5766,9 +5695,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -5789,9 +5718,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", @@ -5916,16 +5845,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -5979,7 +5907,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.6", + "winnow 0.6.8", ] [[package]] @@ -6197,12 +6125,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" -[[package]] -name = "unarray" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" - [[package]] name = "unicase" version = "2.7.0" @@ -6241,15 +6163,15 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "unicode-xid" @@ -6290,11 +6212,11 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.9.1" +version = "2.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97" +checksum = "d11a831e3c0b56e438a28308e7c810799e3c118417f342d30ecec080105395cd" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "log", "once_cell", "url", @@ -6396,20 +6318,11 @@ dependencies = [ "zeroize", ] -[[package]] -name = "wait-timeout" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" -dependencies = [ - "libc", -] - [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -6461,9 +6374,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.89" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -6471,9 +6384,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.89" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", @@ -6486,9 +6399,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.89" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6496,9 +6409,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.89" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", @@ -6509,9 +6422,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.89" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wasm-encoder" @@ -6524,9 +6437,9 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.39.0" +version = "0.207.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111495d6204760238512f57a9af162f45086504da332af210f2f75dd80b34f1d" +checksum = "d996306fb3aeaee0d9157adbe2f670df0236caf19f6728b221e92d0f27b3fe17" dependencies = [ "leb128", ] @@ -6867,30 +6780,31 @@ checksum = "9b6060bc082cc32d9a45587c7640e29e3c7b89ada82677ac25d87850aaccb368" [[package]] name = "wast" -version = "70.0.0" +version = "207.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee4bc54bbe1c6924160b9f75e374a1d07532e7580eb632c0ee6cdd109bb217e" +checksum = "0e40be9fd494bfa501309487d2dc0b3f229be6842464ecbdc54eac2679c84c93" dependencies = [ + "bumpalo", "leb128", "memchr", "unicode-width", - "wasm-encoder 0.39.0", + "wasm-encoder 0.207.0", ] [[package]] name = "wat" -version = "1.0.83" +version = "1.207.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f0dce8cdc288c717cf01e461a1e451a7b8445d53451123536ba576e423a101a" +checksum = "8eb2b15e2d5f300f5e1209e7dc237f2549edbd4203655b6c6cab5cf180561ee7" dependencies = [ "wast", ] [[package]] name = "web-sys" -version = "0.3.66" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", @@ -6933,11 +6847,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -6948,20 +6862,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.51.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.42.2", + "windows-targets 0.52.5", ] [[package]] @@ -6979,22 +6884,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-targets 0.52.5", ] [[package]] @@ -7014,25 +6904,20 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -7041,15 +6926,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -7059,15 +6938,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -7077,15 +6950,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" [[package]] -name = "windows_i686_msvc" -version = "0.42.2" +name = "windows_i686_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -7095,15 +6968,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -7113,15 +6980,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -7131,15 +6992,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -7149,9 +7004,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" @@ -7164,18 +7019,18 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" +checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" dependencies = [ "memchr", ] [[package]] name = "wit-parser" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df4913a2219096373fd6512adead1fb77ecdaa59d7fc517972a7d30b12f625be" +checksum = "316b36a9f0005f5aa4b03c39bc3728d045df136f8c13a73b7db4510dec725e08" dependencies = [ "anyhow", "id-arena", @@ -7220,18 +7075,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", @@ -7294,9 +7149,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.9+zstd.1.5.5" +version = "2.0.10+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index f9e0e077ba6..af9cb2605e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ iroha_data_model_derive = { version = "=2.0.0-pre-rc.21", path = "data_model/der iroha_client = { version = "=2.0.0-pre-rc.21", path = "client" } iroha_config = { version = "=2.0.0-pre-rc.21", path = "config" } iroha_config_base = { version = "=2.0.0-pre-rc.21", path = "config/base" } +iroha_config_base_derive = { version = "=2.0.0-pre-rc.21", path = "config/base/derive" } iroha_schema_gen = { version = "=2.0.0-pre-rc.21", path = "schema/gen" } iroha_schema = { version = "=2.0.0-pre-rc.21", path = "schema", default-features = false } iroha_schema_derive = { version = "=2.0.0-pre-rc.21", path = "schema/derive" } @@ -96,11 +97,13 @@ spinoff = "0.8.0" criterion = "0.5.1" expect-test = "1.5.0" +assertables = "7" eyre = "0.6.12" color-eyre = "0.6.3" thiserror = { version = "1.0.60", default-features = false } displaydoc = { version = "0.2.4", default-features = false } +error-stack = "0.4.1" cfg-if = "1.0.0" derive_more = { version = "0.99.17", default-features = false } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 8fc3a99cf33..8ed2910ba7d 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -54,24 +54,25 @@ iroha_genesis = { workspace = true } iroha_wasm_builder = { workspace = true } clap = { workspace = true, features = ["derive", "env", "string"] } -color-eyre = { workspace = true } eyre = { workspace = true } +error-stack = { workspace = true, features = ["eyre"] } +thiserror = { workspace = true } tracing = { workspace = true } tokio = { workspace = true, features = ["macros", "signal"] } once_cell = { workspace = true } owo-colors = { workspace = true, features = ["supports-colors"] } supports-color = { workspace = true } +toml = { workspace = true } thread-local-panic-hook = { version = "0.1.0", optional = true } [dev-dependencies] serial_test = "3.1.1" tempfile = { workspace = true } -toml = { workspace = true } json5 = { workspace = true } futures = { workspace = true } path-absolutize = { workspace = true } -assertables = "7" +assertables = { workspace = true } [build-dependencies] iroha_wasm_builder = { workspace = true } diff --git a/cli/src/lib.rs b/cli/src/lib.rs index ab9f3b40689..74f524b1640 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -9,8 +9,11 @@ use core::sync::atomic::{AtomicBool, Ordering}; use std::{path::PathBuf, sync::Arc}; use clap::Parser; -use color_eyre::eyre::{eyre, Result, WrapErr}; -use iroha_config::parameters::{actual::Root as Config, user::CliContext}; +use error_stack::{IntoReportCompat, Report, Result, ResultExt}; +use iroha_config::{ + base::{read::ConfigReader, util::Emitter, WithOrigin}, + parameters::{actual::Root as Config, user::Root as UserConfig}, +}; #[cfg(feature = "telemetry")] use iroha_core::metrics::MetricsReporter; use iroha_core::{ @@ -32,7 +35,9 @@ use iroha_core::{ use iroha_data_model::prelude::*; use iroha_genesis::{GenesisNetwork, RawGenesisBlock}; use iroha_logger::{actor::LoggerHandle, InitConfig as LoggerInitConfig}; +use iroha_primitives::addr::SocketAddr; use iroha_torii::Torii; +use thiserror::Error; use tokio::{ signal, sync::{broadcast, mpsc, Notify}, @@ -48,34 +53,44 @@ pub mod samples; /// and queries processing, work of consensus and storage. /// /// # Usage -/// Construct and then use [`Iroha::start()`] or [`Iroha::start_as_task()`]. If you experience +/// Construct and then use [`Iroha::start_torii`] or [`Iroha::start_torii_as_task`]. If you experience /// an immediate shutdown after constructing Iroha, then you probably forgot this step. -#[must_use = "run `.start().await?` to not immediately stop Iroha"] -pub struct Iroha { +#[must_use = "run `.start_torii().await?` to not immediately stop Iroha"] +pub struct Iroha { + main_state: IrohaMainState, + /// Torii web server + torii: ToriiState, +} + +struct IrohaMainState { /// Actor responsible for the configuration - pub kiso: KisoHandle, + _kiso: KisoHandle, /// Queue of transactions - pub queue: Arc, + _queue: Arc, /// Sumeragi consensus - pub sumeragi: SumeragiHandle, + _sumeragi: SumeragiHandle, /// Kura — block storage - pub kura: Arc, - /// Torii web server - pub torii: Option, + kura: Arc, /// Snapshot service. Might be not started depending on the config. - pub snapshot_maker: Option, + _snapshot_maker: Option, /// State of blockchain - pub state: Arc, + state: Arc, /// Thread handlers thread_handlers: Vec, - /// A boolean value indicating whether or not the peers will receive data from the network. /// Used in sumeragi testing. #[cfg(debug_assertions)] pub freeze_status: Arc, } -impl Drop for Iroha { +/// A state of [`Iroha`] for when the network is started, but [`Torii`] not yet. +pub struct ToriiNotStarted(Torii); + +/// A state of [`Iroha`] for when the network & [`Torii`] are started. +#[allow(missing_copy_implementations)] +pub struct ToriiStarted; + +impl Drop for IrohaMainState { fn drop(&mut self) { iroha_logger::trace!("Iroha instance dropped"); let _thread_handles = core::mem::take(&mut self.thread_handlers); @@ -85,6 +100,24 @@ impl Drop for Iroha { } } +/// Error(s) that might occur while starting [`Iroha`] +#[derive(thiserror::Error, Debug, Copy, Clone)] +#[allow(missing_docs)] +pub enum StartError { + #[error("Unable to start peer-to-peer network")] + StartP2p, + #[error("Unable to initialize Kura (block storage)")] + InitKura, + #[error("Unable to start dev telemetry service")] + StartDevTelemetry, + #[error("Unable to start telemetry service")] + StartTelemetry, + #[error("Unable to set up listening for OS signals")] + ListenOsSignal, + #[error("Unable to start Torii (Iroha HTTP API Gateway)")] + StartTorii, +} + struct NetworkRelay { sumeragi: SumeragiHandle, block_sync: BlockSynchronizerHandle, @@ -141,7 +174,7 @@ impl NetworkRelay { } } -impl Iroha { +impl Iroha { fn prepare_panic_hook(notify_shutdown: Arc) { #[cfg(not(feature = "test-network"))] use std::panic::set_hook; @@ -189,7 +222,9 @@ impl Iroha { })); } - /// Create new Iroha instance. + /// Creates new Iroha instance and starts all internal services, except [`Torii`]. + /// + /// Torii is started separately with [`Self::start_torii`] or [`Self::start_torii_as_task`] /// /// # Errors /// - Reading telemetry configs @@ -200,29 +235,34 @@ impl Iroha { /// - Sets global panic hook #[allow(clippy::too_many_lines)] #[iroha_logger::log(name = "init", skip_all)] // This is actually easier to understand as a linear sequence of init statements. - pub async fn new( + pub async fn start_network( config: Config, genesis: Option, logger: LoggerHandle, - ) -> Result { + ) -> Result { let network = IrohaNetwork::start(config.common.key_pair.clone(), config.network.clone()) .await - .wrap_err("Unable to start P2P-network")?; + .change_context(StartError::StartP2p)?; let (events_sender, _) = broadcast::channel(10000); let world = World::with( [genesis_domain(config.genesis.public_key().clone())], - config.sumeragi.trusted_peers.clone(), + config + .sumeragi + .trusted_peers + .value() + .clone() + .into_non_empty_vec(), ); - let kura = Kura::new(&config.kura)?; + let kura = Kura::new(&config.kura).change_context(StartError::InitKura)?; let kura_thread_handler = Kura::start(Arc::clone(&kura)); let live_query_store_handle = LiveQueryStore::from_config(config.live_query_store).start(); - let block_count = kura.init()?; + let block_count = kura.init().change_context(StartError::InitKura)?; let state = match try_read_snapshot( - &config.snapshot.store_dir, + config.snapshot.store_dir.resolve_relative_path(), &kura, live_query_store_handle.clone(), block_count, @@ -284,7 +324,7 @@ impl Iroha { }, }; // Starting Sumeragi requires no async context enabled - let sumeragi = tokio::task::spawn_blocking(move || SumeragiHandle::start(start_args)) + let sumeragi = task::spawn_blocking(move || SumeragiHandle::start(start_args)) .await .expect("Failed to join task with Sumeragi start"); @@ -348,35 +388,52 @@ impl Iroha { Self::prepare_panic_hook(notify_shutdown); - let torii = Some(torii); Ok(Self { - kiso, - queue, - sumeragi, - kura, - torii, - snapshot_maker, - state, - thread_handlers: vec![kura_thread_handler], - #[cfg(debug_assertions)] - freeze_status, + main_state: IrohaMainState { + _kiso: kiso, + _queue: queue, + _sumeragi: sumeragi, + kura, + _snapshot_maker: snapshot_maker, + state, + thread_handlers: vec![kura_thread_handler], + #[cfg(debug_assertions)] + freeze_status, + }, + torii: ToriiNotStarted(torii), }) } + fn take_torii(self) -> (Torii, Iroha) { + let Self { + main_state, + torii: ToriiNotStarted(torii), + } = self; + ( + torii, + Iroha { + main_state, + torii: ToriiStarted, + }, + ) + } + /// To make `Iroha` peer work it should be started first. After /// that moment it will listen for incoming requests and messages. /// /// # Errors /// - Forwards initialisation error. #[iroha_futures::telemetry_future] - pub async fn start(&mut self) -> Result<()> { + pub async fn start_torii(self) -> Result, StartError> { + let (torii, new_self) = self.take_torii(); iroha_logger::info!("Starting Iroha"); - self.torii - .take() - .ok_or_else(|| eyre!("Torii is unavailable. Ensure nothing `take`s the Torii instance before this line"))? + torii .start() .await - .wrap_err("Failed to start Torii") + .into_report() + // https://github.com/hashintel/hash/issues/4295 + .map_err(|report| report.change_context(StartError::StartTorii))?; + Ok(new_self) } /// Starts iroha in separate tokio task. @@ -384,29 +441,45 @@ impl Iroha { /// # Errors /// - Forwards initialisation error. #[cfg(feature = "test-network")] - pub fn start_as_task(&mut self) -> Result>> { + pub fn start_torii_as_task( + self, + ) -> ( + task::JoinHandle>, + Iroha, + ) { + let (torii, new_self) = self.take_torii(); iroha_logger::info!("Starting Iroha as task"); - let torii = self - .torii - .take() - .ok_or_else(|| eyre!("Peer already started in a different task"))?; - Ok(tokio::spawn(async move { - torii.start().await.wrap_err("Failed to start Torii") - })) + let handle = tokio::spawn(async move { + torii + .start() + .await + .into_report() + .map_err(|report| report.change_context(StartError::StartTorii)) + }); + (handle, new_self) } #[cfg(feature = "telemetry")] - async fn start_telemetry(logger: &LoggerHandle, config: &Config) -> Result<()> { + async fn start_telemetry(logger: &LoggerHandle, config: &Config) -> Result<(), StartError> { + const MSG_SUBSCRIBE: &str = "unable to subscribe to the channel"; + const MSG_START_TASK: &str = "unable to start the task"; + #[cfg(feature = "dev-telemetry")] { if let Some(out_file) = &config.dev_telemetry.out_file { let receiver = logger .subscribe_on_telemetry(iroha_logger::telemetry::Channel::Future) .await - .wrap_err("Failed to subscribe on telemetry")?; - let _handle = iroha_telemetry::dev::start_file_output(out_file.clone(), receiver) - .await - .wrap_err("Failed to setup telemetry for futures")?; + .change_context(StartError::StartDevTelemetry) + .attach_printable(MSG_SUBSCRIBE)?; + let _handle = iroha_telemetry::dev::start_file_output( + out_file.resolve_relative_path(), + receiver, + ) + .await + .into_report() + .map_err(|report| report.change_context(StartError::StartDevTelemetry)) + .attach_printable(MSG_START_TASK)?; } } @@ -414,10 +487,13 @@ impl Iroha { let receiver = logger .subscribe_on_telemetry(iroha_logger::telemetry::Channel::Regular) .await - .wrap_err("Failed to subscribe on telemetry")?; + .change_context(StartError::StartTelemetry) + .attach_printable(MSG_SUBSCRIBE)?; let _handle = iroha_telemetry::ws::start(config.clone(), receiver) .await - .wrap_err("Failed to setup telemetry for websocket communication")?; + .into_report() + .map_err(|report| report.change_context(StartError::StartTelemetry)) + .attach_printable(MSG_START_TASK)?; iroha_logger::info!("Telemetry started"); Ok(()) } else { @@ -426,14 +502,16 @@ impl Iroha { } } - fn start_listening_signal(notify_shutdown: Arc) -> Result> { + fn start_listening_signal( + notify_shutdown: Arc, + ) -> Result, StartError> { let (mut sigint, mut sigterm) = signal::unix::signal(signal::unix::SignalKind::interrupt()) .and_then(|sigint| { let sigterm = signal::unix::signal(signal::unix::SignalKind::terminate())?; Ok((sigint, sigterm)) }) - .wrap_err("Failed to start listening for OS signals")?; + .change_context(StartError::ListenOsSignal)?; // NOTE: Triggered by tokio::select #[allow(clippy::redundant_pub_crate)] @@ -485,6 +563,24 @@ impl Iroha { } } +impl Iroha { + #[allow(missing_docs)] + #[cfg(debug_assertions)] + pub fn freeze_status(&self) -> &Arc { + &self.main_state.freeze_status + } + + #[allow(missing_docs)] + pub fn state(&self) -> &Arc { + &self.main_state.state + } + + #[allow(missing_docs)] + pub fn kura(&self) -> &Arc { + &self.main_state.kura + } +} + fn genesis_account(public_key: PublicKey) -> Account { let genesis_account_id = AccountId::new(iroha_genesis::GENESIS_DOMAIN_ID.clone(), public_key); Account::new(genesis_account_id.clone()).build(&genesis_account_id) @@ -502,6 +598,32 @@ fn genesis_domain(public_key: PublicKey) -> Domain { domain } +/// Error of [`read_config_and_genesis`] +#[derive(Error, Debug)] +#[allow(missing_docs)] +pub enum ConfigError { + #[error("Error occurred while reading configuration from file(s) and environment")] + ReadConfig, + #[error("Error occurred while validating configuration integrity")] + ParseConfig, + #[error("Error occurred while reading genesis block")] + ReadGenesis, + #[error("The network consists from this one peer only")] + LonePeer, + #[cfg(feature = "dev-telemetry")] + #[error("Telemetry output file path is root or empty")] + TelemetryOutFileIsRootOrEmpty, + #[cfg(feature = "dev-telemetry")] + #[error("Telemetry output file path is a directory")] + TelemetryOutFileIsDir, + #[error("Torii and Network addresses are the same, but should be different")] + SameNetworkAndToriiAddrs, + #[error("Invalid directory path found")] + InvalidDirPath, + #[error("Network error: cannot listen to address `{addr}`")] + CannotBindAddress { addr: SocketAddr }, +} + /// Read the configuration and then a genesis block if specified. /// /// # Errors @@ -510,19 +632,32 @@ fn genesis_domain(public_key: PublicKey) -> Domain { /// - If failed to build a genesis network pub fn read_config_and_genesis( args: &Args, -) -> Result<(Config, LoggerInitConfig, Option)> { +) -> Result<(Config, LoggerInitConfig, Option), ConfigError> { use iroha_config::parameters::actual::Genesis; - let config = Config::load( - args.config.as_ref(), - CliContext { - submit_genesis: args.submit_genesis, - }, - ) - .wrap_err("failed to load configuration")?; + let mut reader = ConfigReader::new(); + + if let Some(path) = &args.config { + reader = reader + .read_toml_with_extends(path) + .change_context(ConfigError::ReadConfig)?; + } + + let config = reader + .read_and_complete::() + .change_context(ConfigError::ReadConfig)? + .parse() + .change_context(ConfigError::ParseConfig)?; let genesis = if let Genesis::Full { key_pair, file } = &config.genesis { - let raw_block = RawGenesisBlock::from_path(file)?; + let raw_block = RawGenesisBlock::from_path(file.resolve_relative_path()) + .into_report() + // https://github.com/hashintel/hash/issues/4295 + .map_err(|report| { + report + .attach_printable(file.clone().into_attachment().display_path()) + .change_context(ConfigError::ReadGenesis) + })?; Some(GenesisNetwork::new( raw_block, @@ -533,8 +668,46 @@ pub fn read_config_and_genesis( None }; + validate_config(&config, args.submit_genesis)?; + let logger_config = LoggerInitConfig::new(config.logger, args.terminal_colors); + Ok((config, logger_config, genesis)) +} + +fn validate_config(config: &Config, submit_genesis: bool) -> Result<(), ConfigError> { + let mut emitter = Emitter::new(); + + // These cause race condition in tests, due to them actually binding TCP listeners + // Since these validations are primarily for the convenience of the end user, + // it seems a fine compromise to run it only in release mode + #[cfg(release)] + { + validate_try_bind_address(&mut emitter, &config.network.address); + validate_try_bind_address(&mut emitter, &config.torii.address); + } + validate_directory_path(&mut emitter, &config.kura.store_dir); + // maybe validate only if snapshot mode is enabled + validate_directory_path(&mut emitter, &config.snapshot.store_dir); + + if !submit_genesis && !config.sumeragi.contains_other_trusted_peers() { + emitter.emit(Report::new(ConfigError::LonePeer).attach_printable("\ + Reason: the network consists from this one peer only (no `sumeragi.trusted_peers` provided).\n\ + Since `--submit-genesis` is not set, there is no way to receive the genesis block.\n\ + Either provide the genesis by setting `--submit-genesis` argument, `genesis.private_key`,\n\ + and `genesis.file` configuration parameters, or increase the number of trusted peers in\n\ + the network using `sumeragi.trusted_peers` configuration parameter.\ + ").attach_printable(config.sumeragi.trusted_peers.clone().into_attachment().display_as_debug())); + } + + if config.network.address.value() == config.torii.address.value() { + emitter.emit( + Report::new(ConfigError::SameNetworkAndToriiAddrs) + .attach_printable(config.network.address.clone().into_attachment()) + .attach_printable(config.torii.address.clone().into_attachment()), + ); + } + #[cfg(not(feature = "telemetry"))] if config.telemetry.is_some() { // TODO: use a centralized configuration logging @@ -549,7 +722,60 @@ pub fn read_config_and_genesis( eprintln!("`dev_telemetry.out_file` config is specified, but ignored, because Iroha is compiled without `dev-telemetry` feature enabled"); } - Ok((config, logger_config, genesis)) + #[cfg(feature = "dev-telemetry")] + if let Some(path) = &config.dev_telemetry.out_file { + if path.value().parent().is_none() { + emitter.emit( + Report::new(ConfigError::TelemetryOutFileIsRootOrEmpty) + .attach_printable(path.clone().into_attachment().display_path()), + ); + } + if path.value().is_dir() { + emitter.emit( + Report::new(ConfigError::TelemetryOutFileIsDir) + .attach_printable(path.clone().into_attachment().display_path()), + ); + } + } + + emitter.into_result()?; + + Ok(()) +} + +fn validate_directory_path(emitter: &mut Emitter, path: &WithOrigin) { + #[derive(Debug, Error)] + #[error( + "expected path to be either non-existing or a directory, but it points to an existing file: {path}" + )] + struct InvalidDirPathError { + path: PathBuf, + } + + if path.value().is_file() { + emitter.emit( + Report::new(InvalidDirPathError { + path: path.value().clone(), + }) + .attach_printable(path.clone().into_attachment().display_path()) + .change_context(ConfigError::InvalidDirPath), + ); + } +} + +#[cfg(release)] +fn validate_try_bind_address(emitter: &mut Emitter, value: &WithOrigin) { + use std::net::TcpListener; + + if let Err(err) = TcpListener::bind(value.value()) { + emitter.emit( + Report::new(err) + .attach_printable(value.clone().into_attachment()) + .change_context(ConfigError::CannotBindAddress { + addr: value.value().clone(), + }), + ) + } } #[allow(missing_docs)] @@ -572,6 +798,11 @@ pub struct Args { /// Path to the configuration file #[arg(long, short, value_name("PATH"), value_hint(clap::ValueHint::FilePath))] pub config: Option, + /// Enables trace logs of configuration reading & parsing. + /// + /// Might be useful for configuration troubleshooting. + #[arg(long, env)] + pub trace_config: bool, /// Whether to enable ANSI colored output or not /// /// By default, Iroha determines whether the terminal supports colors or not. @@ -622,7 +853,7 @@ mod tests { async fn iroha_should_notify_on_panic() { let notify = Arc::new(Notify::new()); let hook = panic::take_hook(); - ::prepare_panic_hook(Arc::clone(¬ify)); + >::prepare_panic_hook(Arc::clone(¬ify)); let waiters: Vec<_> = repeat(()).take(10).map(|_| Arc::clone(¬ify)).collect(); let handles: Vec<_> = waiters.iter().map(|waiter| waiter.notified()).collect(); thread::spawn(move || { @@ -637,61 +868,45 @@ mod tests { use std::path::PathBuf; use assertables::{assert_contains, assert_contains_as_result}; - use iroha_config::parameters::user::RootPartial as PartialUserConfig; - use iroha_crypto::KeyPair; + use iroha_crypto::{ExposedPrivateKey, KeyPair}; use iroha_primitives::addr::socket_addr; use path_absolutize::Absolutize as _; use super::*; - fn config_factory() -> PartialUserConfig { + fn config_factory() -> toml::Table { let (pubkey, privkey) = KeyPair::random().into_parts(); - - let mut base = PartialUserConfig::default(); - - base.chain_id.set(ChainId::from("0")); - base.public_key.set(pubkey.clone()); - base.private_key.set(privkey.clone()); - base.network.address.set(socket_addr!(127.0.0.1:1337)); - - base.genesis.public_key.set(pubkey); - base.genesis.private_key.set(privkey); - - base.torii.address.set(socket_addr!(127.0.0.1:8080)); - - base - } - - fn config_to_toml_value(config: PartialUserConfig) -> Result { - use iroha_crypto::ExposedPrivateKey; - let private_key = config.private_key.as_ref().unwrap().clone(); - let genesis_private_key = config.genesis.private_key.as_ref().unwrap().clone(); - let mut result = toml::Value::try_from(config)?; - - // private key will be serialized as "[REDACTED PrivateKey]" so need to restore it - result["private_key"] = toml::Value::try_from(ExposedPrivateKey(private_key))?; - result["genesis"]["private_key"] = - toml::Value::try_from(ExposedPrivateKey(genesis_private_key))?; - - Ok(result) + let (genesis_pubkey, genesis_privkey) = KeyPair::random().into_parts(); + + let mut table = toml::Table::new(); + iroha_config::base::toml::Writer::new(&mut table) + .write("chain_id", "0") + .write("public_key", pubkey) + .write("private_key", ExposedPrivateKey(privkey)) + .write(["network", "address"], socket_addr!(127.0.0.1:1337)) + .write(["torii", "address"], socket_addr!(127.0.0.1:8080)) + .write(["genesis", "public_key"], genesis_pubkey) + .write( + ["genesis", "private_key"], + ExposedPrivateKey(genesis_privkey), + ); + table } #[test] - fn relative_file_paths_resolution() -> Result<()> { + fn relative_file_paths_resolution() -> eyre::Result<()> { // Given let genesis = RawGenesisBlockBuilder::default() .executor_file(PathBuf::from("./executor.wasm")) .build(); - let config = { - let mut cfg = config_factory(); - cfg.genesis.file.set("./genesis/gen.json".into()); - cfg.kura.store_dir.set("../storage".into()); - cfg.snapshot.store_dir.set("../snapshots".into()); - cfg.dev_telemetry.out_file.set("../logs/telemetry".into()); - config_to_toml_value(cfg)? - }; + let mut config = config_factory(); + iroha_config::base::toml::Writer::new(&mut config) + .write(["genesis", "file"], "./genesis/gen.json") + .write(["kura", "store_dir"], "../storage") + .write(["snapshot", "store_dir"], "../snapshots") + .write(["dev_telemetry", "out_file"], "../logs/telemetry"); let dir = tempfile::tempdir()?; let genesis_path = dir.path().join("config/genesis/gen.json"); @@ -711,7 +926,9 @@ mod tests { config: Some(config_path), submit_genesis: true, terminal_colors: false, - })?; + trace_config: false, + }) + .map_err(|report| eyre::eyre!("{report:?}"))?; // Then @@ -719,11 +936,15 @@ mod tests { assert!(genesis.is_some()); assert_eq!( - config.kura.store_dir.absolutize()?, + config.kura.store_dir.resolve_relative_path().absolutize()?, dir.path().join("storage") ); assert_eq!( - config.snapshot.store_dir.absolutize()?, + config + .snapshot + .store_dir + .resolve_relative_path() + .absolutize()?, dir.path().join("snapshots") ); assert_eq!( @@ -731,6 +952,7 @@ mod tests { .dev_telemetry .out_file .expect("dev telemetry should be set") + .resolve_relative_path() .absolutize()?, dir.path().join("logs/telemetry") ); @@ -739,18 +961,16 @@ mod tests { } #[test] - fn fails_with_no_trusted_peers_and_submit_role() -> Result<()> { + fn fails_with_no_trusted_peers_and_submit_role() -> eyre::Result<()> { // Given let genesis = RawGenesisBlockBuilder::default() .executor_file(PathBuf::from("./executor.wasm")) .build(); - let config = { - let mut cfg = config_factory(); - cfg.genesis.file.set("./genesis.json".into()); - config_to_toml_value(cfg)? - }; + let mut config = config_factory(); + iroha_config::base::toml::Writer::new(&mut config) + .write(["genesis", "file"], "./genesis.json"); let dir = tempfile::tempdir()?; std::fs::write(dir.path().join("config.toml"), toml::to_string(&config)?)?; @@ -764,6 +984,7 @@ mod tests { config: Some(config_path), submit_genesis: false, terminal_colors: false, + trace_config: false, }) .unwrap_err(); @@ -778,19 +999,17 @@ mod tests { #[test] #[allow(clippy::bool_assert_comparison)] // for expressiveness - fn default_args() -> Result<()> { - let args = Args::try_parse_from(["test"])?; + fn default_args() { + let args = Args::try_parse_from(["test"]).unwrap(); assert_eq!(args.terminal_colors, is_colouring_supported()); assert_eq!(args.submit_genesis, false); - - Ok(()) } #[test] #[allow(clippy::bool_assert_comparison)] // for expressiveness - fn terminal_colors_works_as_expected() -> Result<()> { - fn try_with(arg: &str) -> Result { + fn terminal_colors_works_as_expected() -> eyre::Result<()> { + fn try_with(arg: &str) -> eyre::Result { Ok(Args::try_parse_from(["test", arg])?.terminal_colors) } @@ -807,12 +1026,10 @@ mod tests { } #[test] - fn user_provided_config_path_works() -> Result<()> { - let args = Args::try_parse_from(["test", "--config", "/home/custom/file.json"])?; + fn user_provided_config_path_works() { + let args = Args::try_parse_from(["test", "--config", "/home/custom/file.json"]).unwrap(); assert_eq!(args.config, Some(PathBuf::from("/home/custom/file.json"))); - - Ok(()) } #[test] diff --git a/cli/src/main.rs b/cli/src/main.rs index fe90dcd3a26..0575126bb93 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -2,19 +2,44 @@ use std::env; use clap::Parser; -use color_eyre::eyre::Result; +use error_stack::{IntoReportCompat, ResultExt}; use iroha::Args; +#[derive(thiserror::Error, Debug)] +enum MainError { + #[error("Could not set up configuration tracing")] + TraceConfigSetup, + #[error("Configuration error")] + Config, + #[error("Could not initialize logger")] + Logger, + #[error("Could not start Iroha")] + IrohaStart, +} + #[tokio::main] -async fn main() -> Result<()> { +async fn main() -> error_stack::Result<(), MainError> { let args = Args::parse(); - if args.terminal_colors { - color_eyre::install()?; + configure_reports(&args); + + if args.trace_config { + iroha_config::enable_tracing() + .change_context(MainError::TraceConfigSetup) + .attach_printable("was enabled by `--trace-config` argument")?; } - let (config, logger_config, genesis) = iroha::read_config_and_genesis(&args)?; - let logger = iroha_logger::init_global(logger_config)?; + let (config, logger_config, genesis) = + iroha::read_config_and_genesis(&args).change_context(MainError::Config).attach_printable_lazy(|| { + args.config.as_ref().map_or_else( + || "`--config` arg was not set, therefore configuration relies fully on environment variables".to_owned(), + |path| format!("config path is specified by `--config` arg: {}", path.display()), + ) + })?; + let logger = iroha_logger::init_global(logger_config) + .into_report() + // https://github.com/hashintel/hash/issues/4295 + .map_err(|report| report.change_context(MainError::Logger))?; iroha_logger::info!( version = env!("CARGO_PKG_VERSION"), @@ -26,10 +51,28 @@ async fn main() -> Result<()> { iroha_logger::debug!("Submitting genesis."); } - iroha::Iroha::new(config, genesis, logger) - .await? - .start() - .await?; + let _iroha = iroha::Iroha::start_network(config, genesis, logger) + .await + .change_context(MainError::IrohaStart)? + .start_torii() + .await + .change_context(MainError::IrohaStart)?; Ok(()) } + +/// Configures globals of [`error_stack::Report`] +fn configure_reports(args: &Args) { + use std::panic::Location; + + use error_stack::{fmt::ColorMode, Report}; + + Report::set_color_mode(if args.terminal_colors { + ColorMode::Color + } else { + ColorMode::None + }); + + // neither devs nor users benefit from it + Report::install_debug_hook::(|_, _| {}); +} diff --git a/cli/src/samples.rs b/cli/src/samples.rs index fe45f22e104..c3be97d1e6b 100644 --- a/cli/src/samples.rs +++ b/cli/src/samples.rs @@ -1,15 +1,8 @@ //! This module contains the sample configurations used for testing and benchmarking throughout Iroha. -use std::{collections::HashSet, path::Path, str::FromStr, time::Duration}; +use std::{collections::HashSet, path::Path, str::FromStr}; -use iroha_config::{ - base::{HumanDuration, UnwrapPartial}, - parameters::{ - actual::Root as Config, - user::{CliContext, RootPartial as UserConfig}, - }, - snapshot::Mode as SnapshotMode, -}; -use iroha_crypto::{KeyPair, PublicKey}; +use iroha_config::{base::toml::TomlSource, parameters::actual::Root as Config}; +use iroha_crypto::{ExposedPrivateKey, KeyPair, PublicKey}; use iroha_data_model::{peer::PeerId, prelude::*, ChainId}; use iroha_primitives::{ addr::{socket_addr, SocketAddr}, @@ -52,79 +45,64 @@ pub fn get_trusted_peers(public_key: Option<&PublicKey>) -> HashSet { } #[allow(clippy::implicit_hasher)] -/// Get a sample Iroha configuration on user-layer level. Trusted peers must be -/// specified in this function, including the current peer. Use [`get_trusted_peers`] -/// to populate `trusted_peers` if in doubt. Almost equivalent to the [`get_config`] -/// function, except the proxy is left unbuilt. +/// Sample Iroha configuration in an unparsed format. /// -/// # Panics -/// - when [`KeyPair`] generation fails (rare case). -pub fn get_user_config( - peers: &UniqueVec, - chain_id: Option, - peer_key_pair: Option, - genesis_key_pair: Option, -) -> UserConfig { - let chain_id = chain_id.unwrap_or_else(|| ChainId::from("0")); - - let (peer_public_key, peer_private_key) = - peer_key_pair.unwrap_or_else(KeyPair::random).into_parts(); - iroha_logger::info!(%peer_public_key); - let (genesis_public_key, genesis_private_key) = genesis_key_pair - .unwrap_or_else(KeyPair::random) - .into_parts(); - iroha_logger::info!(%genesis_public_key); +/// [`get_config`] gives the parsed, complete version of it. +/// +/// Trusted peers must either be specified in this function, including the current peer. Use [`get_trusted_peers`] +/// to populate `trusted_peers` if in doubt. +pub fn get_config_toml( + peers: UniqueVec, + chain_id: ChainId, + peer_key_pair: KeyPair, + genesis_key_pair: KeyPair, +) -> toml::Table { + let (public_key, private_key) = peer_key_pair.clone().into_parts(); + let (genesis_public_key, genesis_private_key) = genesis_key_pair.into_parts(); - let mut config = UserConfig::new(); + iroha_logger::info!(%public_key, "sample configuration public key"); - config.chain_id.set(chain_id); - config.public_key.set(peer_public_key); - config.private_key.set(peer_private_key); - config.network.address.set(DEFAULT_P2P_ADDR); - config - .chain_wide - .max_transactions_in_block - .set(2.try_into().unwrap()); - config.sumeragi.trusted_peers.set(peers.to_vec()); - config.torii.address.set(DEFAULT_TORII_ADDR); - config - .network - .block_gossip_max_size - .set(1.try_into().unwrap()); - config - .network - .block_gossip_period - .set(HumanDuration(Duration::from_millis(500))); - config.genesis.private_key.set(genesis_private_key); - config.genesis.public_key.set(genesis_public_key); - config.genesis.file.set("./genesis.json".into()); - // There is no need in persistency in tests - // If required to should be set explicitly not to overlap with other existing tests - config.snapshot.mode.set(SnapshotMode::Disabled); + let mut raw = toml::Table::new(); + iroha_config::base::toml::Writer::new(&mut raw) + .write("chain_id", chain_id) + .write("public_key", public_key) + .write("private_key", ExposedPrivateKey(private_key)) + .write(["sumeragi", "trusted_peers"], peers) + .write(["network", "address"], DEFAULT_P2P_ADDR) + .write(["network", "block_gossip_period"], 500) + .write(["network", "block_gossip_max_size"], 1) + .write(["torii", "address"], DEFAULT_TORII_ADDR) + .write(["chain_wide", "max_transactions_in_block"], 2) + .write(["genesis", "public_key"], genesis_public_key) + .write( + ["genesis", "private_key"], + ExposedPrivateKey(genesis_private_key), + ) + .write(["genesis", "file"], "NEVER READ ME; YOU FOUND A BUG!") + // There is no need in persistence in tests. + // If required to should be set explicitly not to overlap with other existing tests + .write(["snapshot", "mode"], "disabled"); - config + raw } #[allow(clippy::implicit_hasher)] /// Get a sample Iroha configuration. Trusted peers must either be /// specified in this function, including the current peer. Use [`get_trusted_peers`] /// to populate `trusted_peers` if in doubt. -/// -/// # Panics -/// - when [`KeyPair`] generation fails (rare case). pub fn get_config( - trusted_peers: &UniqueVec, - chain_id: Option, - peer_key_pair: Option, - genesis_key_pair: Option, + trusted_peers: UniqueVec, + chain_id: ChainId, + peer_key_pair: KeyPair, + genesis_key_pair: KeyPair, ) -> Config { - get_user_config(trusted_peers, chain_id, peer_key_pair, genesis_key_pair) - .unwrap_partial() - .expect("config should build as all required fields were provided") - .parse(CliContext { - submit_genesis: true, - }) - .expect("config should finalize as the input is semantically valid (or there is a bug)") + Config::from_toml_source(TomlSource::inline(get_config_toml( + trusted_peers, + chain_id, + peer_key_pair, + genesis_key_pair, + ))) + .expect("should be a valid config") } /// Construct executor from path. @@ -136,7 +114,7 @@ pub fn get_config( /// - Failed to create temp dir for executor output /// - Failed to build executor /// - Failed to optimize executor -pub fn construct_executor

(relative_path: &P) -> color_eyre::Result +pub fn construct_executor

(relative_path: &P) -> eyre::Result where P: AsRef + ?Sized, { diff --git a/client/Cargo.toml b/client/Cargo.toml index f9f316fa0a9..485f8556eef 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -49,8 +49,10 @@ tls-rustls-webpki-roots = [ [dependencies] iroha_config = { workspace = true } +iroha_config_base = { workspace = true } iroha_crypto = { workspace = true } -iroha_data_model = { workspace = true, features = ["http"] } +# FIXME: should remove `transparent_api` feature? +iroha_data_model = { workspace = true, features = ["http", "transparent_api"] } iroha_primitives = { workspace = true } iroha_logger = { workspace = true } iroha_telemetry = { workspace = true } @@ -60,6 +62,7 @@ test_samples = { workspace = true } attohttpc = { version = "0.28.0", default-features = false } eyre = { workspace = true } +error-stack = { workspace = true } http = "1.1.0" url = { workspace = true } rand = { workspace = true } @@ -75,7 +78,6 @@ tokio = { workspace = true, features = ["rt"] } tokio-tungstenite = { workspace = true } tungstenite = { workspace = true } futures-util = "0.3.30" -merge = "0.1.0" toml = { workspace = true } nonzero_ext = { workspace = true } diff --git a/client/benches/torii.rs b/client/benches/torii.rs index a8bc5c97ab0..ca0b809c69f 100644 --- a/client/benches/torii.rs +++ b/client/benches/torii.rs @@ -22,10 +22,10 @@ fn query_requests(criterion: &mut Criterion) { let chain_id = get_chain_id(); let configuration = get_config( - &unique_vec![peer.id.clone()], - Some(chain_id.clone()), - Some(get_key_pair(test_network::Signatory::Peer)), - Some(get_key_pair(test_network::Signatory::Genesis)), + unique_vec![peer.id.clone()], + chain_id.clone(), + get_key_pair(test_network::Signatory::Peer), + get_key_pair(test_network::Signatory::Genesis), ); let rt = Runtime::test(); @@ -127,10 +127,10 @@ fn instruction_submits(criterion: &mut Criterion) { let chain_id = get_chain_id(); let configuration = get_config( - &unique_vec![peer.id.clone()], - Some(chain_id.clone()), - Some(get_key_pair(test_network::Signatory::Peer)), - Some(get_key_pair(test_network::Signatory::Genesis)), + unique_vec![peer.id.clone()], + chain_id.clone(), + get_key_pair(test_network::Signatory::Peer), + get_key_pair(test_network::Signatory::Genesis), ); let genesis = GenesisNetwork::new( RawGenesisBlockBuilder::default() diff --git a/client/benches/tps/utils.rs b/client/benches/tps/utils.rs index afc4cb5d598..913aca11688 100644 --- a/client/benches/tps/utils.rs +++ b/client/benches/tps/utils.rs @@ -114,7 +114,7 @@ impl Config { .iroha .as_ref() .expect("Must be some") - .state + .state() .view(); let mut blocks = state_view.all_blocks().skip(blocks_out_of_measure as usize); let (txs_accepted, txs_rejected) = (0..self.blocks) diff --git a/client/examples/million_accounts_genesis.rs b/client/examples/million_accounts_genesis.rs index 03114c82088..013e2fd0d3b 100644 --- a/client/examples/million_accounts_genesis.rs +++ b/client/examples/million_accounts_genesis.rs @@ -39,10 +39,10 @@ fn main_genesis() { let chain_id = get_chain_id(); let configuration = get_config( - &unique_vec![peer.id.clone()], - Some(chain_id.clone()), - Some(get_key_pair(test_network::Signatory::Peer)), - Some(get_key_pair(test_network::Signatory::Genesis)), + unique_vec![peer.id.clone()], + chain_id.clone(), + get_key_pair(test_network::Signatory::Peer), + get_key_pair(test_network::Signatory::Genesis), ); let rt = Runtime::test(); let genesis = GenesisNetwork::new( diff --git a/client/examples/register_1000_triggers.rs b/client/examples/register_1000_triggers.rs index cbdeb2db807..cc9542fd9b9 100644 --- a/client/examples/register_1000_triggers.rs +++ b/client/examples/register_1000_triggers.rs @@ -60,10 +60,10 @@ fn main() -> Result<(), Box> { let chain_id = get_chain_id(); let mut configuration = get_config( - &unique_vec![peer.id.clone()], - Some(chain_id.clone()), - Some(get_key_pair(test_network::Signatory::Peer)), - Some(get_key_pair(test_network::Signatory::Genesis)), + unique_vec![peer.id.clone()], + chain_id.clone(), + get_key_pair(test_network::Signatory::Peer), + get_key_pair(test_network::Signatory::Genesis), ); // Increase executor limits for large genesis diff --git a/client/src/config.rs b/client/src/config.rs index 70a8f1bab3a..b64ef2881cf 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -4,11 +4,9 @@ use core::str::FromStr; use std::{path::Path, time::Duration}; use derive_more::Display; +use error_stack::ResultExt; use eyre::Result; -use iroha_config::{ - base, - base::{FromEnv, StdEnv, UnwrapPartial}, -}; +use iroha_config_base::{read::ConfigReader, toml::TomlSource}; use iroha_crypto::KeyPair; use iroha_data_model::{prelude::*, ChainId}; use iroha_primitives::small::SmallStr; @@ -16,8 +14,6 @@ use serde::{Deserialize, Serialize}; use serde_with::{DeserializeFromStr, SerializeDisplay}; use url::Url; -use crate::config::user::RootPartial; - mod user; #[allow(missing_docs)] @@ -71,16 +67,25 @@ pub struct Config { pub transaction_add_nonce: bool, } +/// An error type for [`Config::load`] +#[derive(thiserror::Error, Debug, Copy, Clone)] +#[error("Failed to load configuration")] +pub struct LoadError; + impl Config { /// Loads configuration from a file /// /// # Errors /// - unable to load config from a TOML file /// - the config is invalid - pub fn load(path: impl AsRef) -> std::result::Result { - let config = RootPartial::from_toml(path)?; - let config = config.merge(RootPartial::from_env(&StdEnv)?); - Ok(config.unwrap_partial()?.parse()?) + pub fn load(path: impl AsRef) -> error_stack::Result { + let config = ConfigReader::new() + .with_toml_source(TomlSource::from_file(path).change_context(LoadError)?) + .read_and_complete::() + .change_context(LoadError)? + .parse() + .change_context(LoadError)?; + Ok(config) } } @@ -100,7 +105,7 @@ mod tests { #[test] fn parse_full_toml_config() { - let _: RootPartial = toml::toml! { + let input = toml::toml! { chain_id = "00000000-0000-0000-0000-000000000000" torii_url = "http://127.0.0.1:8080/" @@ -117,6 +122,11 @@ mod tests { time_to_live = 100_000 status_timeout = 100_000 nonce = false - }.try_into().unwrap(); + }; + + ConfigReader::new() + .with_toml_source(TomlSource::inline(input)) + .read_and_complete::() + .unwrap(); } } diff --git a/client/src/config/user.rs b/client/src/config/user.rs index 1ebb11e3a9c..a909509f831 100644 --- a/client/src/config/user.rs +++ b/client/src/config/user.rs @@ -1,73 +1,56 @@ //! User configuration view. -mod boilerplate; - -use std::{fs::File, io::Read, path::Path, str::FromStr, time::Duration}; - -pub use boilerplate::*; -use eyre::{eyre, Context, Report}; -use iroha_config::base::{Emitter, ErrorsCollection}; +use std::str::FromStr; + +use error_stack::{Report, ResultExt}; +use iroha_config_base::{ + attach::ConfigValueAndOrigin, + util::{Emitter, EmitterResultExt, HumanDuration}, + ReadConfig, WithOrigin, +}; use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; use iroha_data_model::prelude::{AccountId, ChainId, DomainId}; -use merge::Merge; use serde_with::DeserializeFromStr; use url::Url; use crate::config::BasicAuth; -impl RootPartial { - /// Reads the partial layer from TOML - /// - /// # Errors - /// - File not found - /// - Not valid TOML or content - pub fn from_toml(path: impl AsRef) -> eyre::Result { - let contents = { - let mut contents = String::new(); - File::open(path.as_ref()) - .wrap_err_with(|| { - eyre!("cannot open file at location `{}`", path.as_ref().display()) - })? - .read_to_string(&mut contents)?; - contents - }; - let layer: Self = toml::from_str(&contents).wrap_err("failed to parse toml")?; - Ok(layer) - } - - /// Merge other into self - #[must_use] - pub fn merge(mut self, other: Self) -> Self { - Merge::merge(&mut self, other); - self - } -} - /// Root of the user configuration -#[derive(Clone, Debug)] +#[derive(Clone, Debug, ReadConfig)] #[allow(missing_docs)] pub struct Root { pub chain_id: ChainId, + #[config(env = "TORII_URL")] pub torii_url: OnlyHttpUrl, pub basic_auth: Option, + #[config(nested)] pub account: Account, + #[config(nested)] pub transaction: Transaction, } +#[derive(thiserror::Error, Debug)] +pub enum ParseError { + #[error("Transaction status timeout should be smaller than its time-to-live")] + TxTimeoutVsTtl, + #[error("Failed to construct a key pair from provided public and private keys")] + KeyPair, +} + impl Root { /// Validates user configuration for semantic errors and constructs a complete /// [`super::Config`]. /// /// # Errors /// If a set of validity errors occurs. - pub fn parse(self) -> Result> { + pub fn parse(self) -> error_stack::Result { let Self { chain_id, torii_url, basic_auth, account: Account { - domian_id, + domain_id, public_key, private_key, }, @@ -83,29 +66,26 @@ impl Root { // TODO: validate if TTL is too small? - if tx_timeout > tx_ttl { - // TODO: - // would be nice to provide a nice report with spans in the input - // pointing out source data in provided config files - // FIXME: explain why it should be smaller - emitter.emit(eyre!( - "transaction status timeout should be smaller than its time-to-live" - )) + if tx_timeout.value() > tx_ttl.value() { + emitter.emit( + Report::new(ParseError::TxTimeoutVsTtl) + .attach_printable(tx_timeout.clone().into_attachment()) + .attach_printable(tx_ttl.clone().into_attachment()) + // FIXME: is this correct? + .attach_printable("Note: it doesn't make sense to set the timeout longer than the possible transaction lifetime"), + ) } - let account_id = AccountId::new(domian_id, public_key.clone()); - + let (public_key, public_key_origin) = public_key.into_tuple(); + let (private_key, private_key_origin) = private_key.into_tuple(); + let account_id = AccountId::new(domain_id, public_key.clone()); let key_pair = KeyPair::new(public_key, private_key) - .wrap_err("failed to construct a key pair") - .map_or_else( - |err| { - emitter.emit(err); - None - }, - Some, - ); + .attach_printable(ConfigValueAndOrigin::new("[REDACTED]", public_key_origin)) + .attach_printable(ConfigValueAndOrigin::new("[REDACTED]", private_key_origin)) + .change_context(ParseError::KeyPair) + .ok_or_emit(&mut emitter); - emitter.finish()?; + emitter.into_result()?; Ok(super::Config { chain_id, @@ -113,26 +93,29 @@ impl Root { key_pair: key_pair.unwrap(), torii_api_url: torii_url.0, basic_auth, - transaction_ttl: tx_ttl, - transaction_status_timeout: tx_timeout, + transaction_ttl: tx_ttl.into_value().get(), + transaction_status_timeout: tx_timeout.into_value().get(), transaction_add_nonce: tx_add_nonce, }) } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, ReadConfig)] #[allow(missing_docs)] pub struct Account { - pub domian_id: DomainId, - pub public_key: PublicKey, - pub private_key: PrivateKey, + pub domain_id: DomainId, + pub public_key: WithOrigin, + pub private_key: WithOrigin, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, ReadConfig)] #[allow(missing_docs)] pub struct Transaction { - pub time_to_live: Duration, - pub status_timeout: Duration, + #[config(default = "super::DEFAULT_TRANSACTION_TIME_TO_LIVE.into()")] + pub time_to_live: WithOrigin, + #[config(default = "super::DEFAULT_TRANSACTION_STATUS_TIMEOUT.into()")] + pub status_timeout: WithOrigin, + #[config(default = "super::DEFAULT_TRANSACTION_NONCE")] pub nonce: bool, } @@ -173,17 +156,21 @@ pub enum ParseHttpUrlError { mod tests { use std::collections::HashSet; - use iroha_config::base::{FromEnv as _, TestEnv}; + use iroha_config_base::{env::MockEnv, read::ConfigReader}; use super::*; #[test] fn parses_all_envs() { - let env = TestEnv::new().set("TORII_URL", "http://localhost:8080"); + let env = MockEnv::from([("TORII_URL", "http://localhost:8080")]); - let _layer = RootPartial::from_env(&env).expect("should not fail since env is valid"); + let _ = ConfigReader::new() + .with_env(env.clone()) + .read_and_complete::() + .expect_err("there are missing fields, but that of no concern"); - assert_eq!(env.unvisited(), HashSet::new()) + assert_eq!(env.unvisited(), HashSet::new()); + assert_eq!(env.unknown(), HashSet::new()); } #[test] diff --git a/client/src/config/user/boilerplate.rs b/client/src/config/user/boilerplate.rs index 635f9ebb153..e69de29bb2d 100644 --- a/client/src/config/user/boilerplate.rs +++ b/client/src/config/user/boilerplate.rs @@ -1,147 +0,0 @@ -//! Code to be generated by a proc macro in future - -#![allow(missing_docs)] - -use std::error::Error; - -use iroha_config::base::{ - Emitter, FromEnv, HumanDuration, Merge, ParseEnvResult, UnwrapPartial, UnwrapPartialResult, - UserField, -}; -use iroha_crypto::{PrivateKey, PublicKey}; -use iroha_data_model::{domain::DomainId, ChainId}; -use serde::Deserialize; - -use crate::config::{ - base::{FromEnvResult, ReadEnv}, - user::{Account, OnlyHttpUrl, Root, Transaction}, - BasicAuth, DEFAULT_TRANSACTION_NONCE, DEFAULT_TRANSACTION_STATUS_TIMEOUT, - DEFAULT_TRANSACTION_TIME_TO_LIVE, -}; - -#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct RootPartial { - pub chain_id: UserField, - pub torii_url: UserField, - pub basic_auth: UserField, - pub account: AccountPartial, - pub transaction: TransactionPartial, -} - -impl RootPartial { - #[allow(unused)] - pub fn new() -> Self { - // TODO: gen with macro - Self::default() - } -} - -impl FromEnv for RootPartial { - fn from_env>(env: &R) -> FromEnvResult - where - Self: Sized, - { - let mut emitter = Emitter::new(); - - let torii_url = - ParseEnvResult::parse_simple(&mut emitter, env, "TORII_URL", "torii_url").into(); - - emitter.finish()?; - - Ok(Self { - chain_id: None.into(), - torii_url, - basic_auth: None.into(), - account: AccountPartial::default(), - transaction: TransactionPartial::default(), - }) - } -} - -impl UnwrapPartial for RootPartial { - type Output = Root; - - fn unwrap_partial(self) -> UnwrapPartialResult { - let mut emitter = Emitter::new(); - - if self.chain_id.is_none() { - emitter.emit_missing_field("chain_id"); - } - if self.torii_url.is_none() { - emitter.emit_missing_field("torii_url"); - } - let account = emitter.try_unwrap_partial(self.account); - let transaction = emitter.try_unwrap_partial(self.transaction); - - emitter.finish()?; - - Ok(Root { - chain_id: self.chain_id.get().unwrap(), - torii_url: self.torii_url.get().unwrap(), - basic_auth: self.basic_auth.get(), - account: account.unwrap(), - transaction: transaction.unwrap(), - }) - } -} - -#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct AccountPartial { - pub domain_id: UserField, - pub public_key: UserField, - pub private_key: UserField, -} - -impl UnwrapPartial for AccountPartial { - type Output = Account; - - fn unwrap_partial(self) -> UnwrapPartialResult { - let mut emitter = Emitter::new(); - - if self.domain_id.is_none() { - emitter.emit_missing_field("account.domain_id"); - } - if self.public_key.is_none() { - emitter.emit_missing_field("account.public_key"); - } - if self.private_key.is_none() { - emitter.emit_missing_field("account.private_key"); - } - - emitter.finish()?; - - Ok(Account { - domian_id: self.domain_id.get().unwrap(), - public_key: self.public_key.get().unwrap(), - private_key: self.private_key.get().unwrap(), - }) - } -} - -#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct TransactionPartial { - pub time_to_live: UserField, - pub status_timeout: UserField, - pub nonce: UserField, -} - -impl UnwrapPartial for TransactionPartial { - type Output = Transaction; - - fn unwrap_partial(self) -> UnwrapPartialResult { - Ok(Transaction { - time_to_live: self - .time_to_live - .get() - .map_or(DEFAULT_TRANSACTION_TIME_TO_LIVE, HumanDuration::get), - status_timeout: self - .status_timeout - .get() - .map_or(DEFAULT_TRANSACTION_STATUS_TIMEOUT, HumanDuration::get), - nonce: self.nonce.get().unwrap_or(DEFAULT_TRANSACTION_NONCE), - }) - } -} diff --git a/client/tests/integration/events/pipeline.rs b/client/tests/integration/events/pipeline.rs index cd8288e0f05..778cb29615f 100644 --- a/client/tests/integration/events/pipeline.rs +++ b/client/tests/integration/events/pipeline.rs @@ -128,7 +128,7 @@ fn applied_block_must_be_available_in_kura() { peer.iroha .as_ref() .expect("Must be some") - .kura + .kura() .get_block_by_height(event.header().height()) .expect("Block applied event was received earlier"); } diff --git a/client/tests/integration/extra_functional/connected_peers.rs b/client/tests/integration/extra_functional/connected_peers.rs index 0bf17809514..000a352ce3d 100644 --- a/client/tests/integration/extra_functional/connected_peers.rs +++ b/client/tests/integration/extra_functional/connected_peers.rs @@ -39,7 +39,7 @@ fn register_new_peer() -> Result<()> { // Start new peer let mut configuration = Config::test(); - configuration.sumeragi.trusted_peers = + configuration.sumeragi.trusted_peers.value_mut().others = unique_vec![peer_clients.choose(&mut thread_rng()).unwrap().0.id.clone()]; let rt = Runtime::test(); let new_peer = rt.block_on( diff --git a/client/tests/integration/triggers/time_trigger.rs b/client/tests/integration/triggers/time_trigger.rs index a7d161eb033..9d9ec6379ef 100644 --- a/client/tests/integration/triggers/time_trigger.rs +++ b/client/tests/integration/triggers/time_trigger.rs @@ -5,7 +5,7 @@ use iroha_client::{ client::{self, Client, QueryResult}, data_model::{prelude::*, transaction::WasmSmartContract}, }; -use iroha_config::parameters::defaults::chain_wide::DEFAULT_CONSENSUS_ESTIMATION; +use iroha_config::parameters::defaults::chain_wide::CONSENSUS_ESTIMATION as DEFAULT_CONSENSUS_ESTIMATION; use iroha_data_model::events::pipeline::{BlockEventFilter, BlockStatus}; use iroha_logger::info; use test_network::*; diff --git a/client_cli/Cargo.toml b/client_cli/Cargo.toml index 821411b64fc..8b0d7d46383 100644 --- a/client_cli/Cargo.toml +++ b/client_cli/Cargo.toml @@ -27,13 +27,15 @@ iroha_client = { workspace = true } iroha_primitives = { workspace = true } iroha_config_base = { workspace = true } -color-eyre = { workspace = true } +thiserror = { workspace = true } +error-stack = { workspace = true, features = ["eyre"] } +eyre = { workspace = true } clap = { workspace = true, features = ["derive"] } -dialoguer = { version = "0.11.0", default-features = false } json5 = { workspace = true } once_cell = { workspace = true } serde_json = { workspace = true } erased-serde = "0.4.5" +supports-color = { workspace = true } [build-dependencies] vergen = { version = "8.3.1", default-features = false } diff --git a/client_cli/src/main.rs b/client_cli/src/main.rs index 1cfa5487f2d..90836ebc07f 100644 --- a/client_cli/src/main.rs +++ b/client_cli/src/main.rs @@ -6,18 +6,16 @@ use std::{ str::FromStr, }; -use color_eyre::{ - eyre::{eyre, Error, WrapErr}, - Result, -}; -// FIXME: sync with `kagami` (it uses `inquiry`, migrate both to something single) use erased_serde::Serialize; +use error_stack::{fmt::ColorMode, IntoReportCompat, ResultExt}; +use eyre::{eyre, Error, Result, WrapErr}; use iroha_client::{ client::{Client, QueryResult}, config::Config, data_model::{metadata::MetadataValueBox, prelude::*}, }; use iroha_primitives::addr::SocketAddr; +use thiserror::Error; /// Re-usable clap `--metadata ` (`-m`) argument. /// Should be combined with `#[command(flatten)]` attr. @@ -45,7 +43,7 @@ impl MetadataArgs { path.display() ) })?; - Ok::<_, color_eyre::Report>(metadata) + Ok::<_, eyre::Report>(metadata) }) .transpose()?; @@ -176,22 +174,35 @@ impl RunArgs for Subcommand { } } -fn main() -> Result<()> { - color_eyre::install()?; +#[derive(Error, Debug)] +enum MainError { + #[error("Failed to load Iroha Client CLI configuration")] + Config, + #[error("Failed to serialize config")] + SerializeConfig, + #[error("Failed to run the command")] + Subcommand, +} +fn main() -> error_stack::Result<(), MainError> { let Args { config: config_path, subcommand, verbose, } = clap::Parser::parse(); - let config = Config::load(config_path)?; + error_stack::Report::set_color_mode(color_mode()); + let config = Config::load(config_path) + // FIXME: would be nice to NOT change the context, it's unnecessary + .change_context(MainError::Config) + .attach_printable("config path was set by `--config` argument")?; if verbose { eprintln!( "Configuration: {}", &serde_json::to_string_pretty(&config) - .wrap_err("Failed to serialize configuration.")? + .change_context(MainError::SerializeConfig) + .attach_printable("caused by `--verbose` argument")? ); } @@ -199,8 +210,22 @@ fn main() -> Result<()> { write: stdout(), config, }; + subcommand + .run(&mut context) + .into_report() + .map_err(|report| report.change_context(MainError::Subcommand))?; + + Ok(()) +} - subcommand.run(&mut context) +fn color_mode() -> ColorMode { + if supports_color::on(supports_color::Stream::Stdout).is_some() + && supports_color::on(supports_color::Stream::Stderr).is_some() + { + ColorMode::Color + } else { + ColorMode::None + } } /// Submit instruction with metadata to network. diff --git a/config/Cargo.toml b/config/Cargo.toml index 032d64b2176..556b9f1bfa5 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -11,13 +11,13 @@ license.workspace = true workspace = true [dependencies] -iroha_config_base = { workspace = true, features = ["json"] } +iroha_config_base = { workspace = true } iroha_data_model = { workspace = true } iroha_primitives = { workspace = true } iroha_crypto = { workspace = true } iroha_genesis = { workspace = true } -eyre = { workspace = true } +error-stack = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["fmt", "ansi"] } url = { workspace = true, features = ["serde"] } @@ -33,12 +33,11 @@ derive_more = { workspace = true } cfg-if = { workspace = true } once_cell = { workspace = true } nonzero_ext = { workspace = true } -toml = { workspace = true } -merge = "0.1.0" +hex = { workspace = true, features = ["std"] } + +# for tracing +stderrlog = "0.6.0" [dev-dependencies] -proptest = "1.4.0" -stacker = "0.1.15" expect-test = { workspace = true } -trybuild = { workspace = true } -hex = { workspace = true } +assertables = { workspace = true } diff --git a/config/base/Cargo.toml b/config/base/Cargo.toml index 245e5219ed7..e39f0b752cf 100644 --- a/config/base/Cargo.toml +++ b/config/base/Cargo.toml @@ -10,21 +10,21 @@ license.workspace = true [lints] workspace = true -[features] -# enables some JSON-related features -json = ["serde_json"] - [dependencies] -merge = "0.1.0" +iroha_config_base_derive = { workspace = true } + drop_bomb = { workspace = true } -derive_more = { workspace = true, features = ["from", "deref", "deref_mut"] } -eyre = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_with = { workspace = true, features = ["macros", "std"] } thiserror = { workspace = true } num-traits = "0.2.19" - -serde_json = { version = "1", optional = true } +toml = { workspace = true } +error-stack = { workspace = true } +log = "0.4" +derive_more = { workspace = true, features = ["constructor", "display"] } [dev-dependencies] -toml = { workspace = true } +expect-test = { workspace = true } +strum = { workspace = true, features = ["derive", "std"] } +hex = { workspace = true, features = ["std"] } +serde_with = { workspace = true, features = ["hex"] } diff --git a/config/base/derive/Cargo.toml b/config/base/derive/Cargo.toml new file mode 100644 index 00000000000..ec87ac8c2c8 --- /dev/null +++ b/config/base/derive/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "iroha_config_base_derive" + +edition.workspace = true +version.workspace = true +authors.workspace = true + +license.workspace = true + +[lints] +workspace = true + +[lib] +proc-macro = true + +[dependencies] +syn = { workspace = true, features = ["default", "full", "extra-traits", "visit-mut"] } +quote = { workspace = true } +darling = { workspace = true } +proc-macro2 = { workspace = true } +manyhow = { workspace = true } +iroha_macro_utils = { workspace = true } + +[dev-dependencies] +trybuild = { workspace = true } +iroha_config_base = { workspace = true } +expect-test = { workspace = true } +serde = { workspace = true, features = ["derive"] } diff --git a/config/base/derive/src/lib.rs b/config/base/derive/src/lib.rs new file mode 100644 index 00000000000..8d202d51617 --- /dev/null +++ b/config/base/derive/src/lib.rs @@ -0,0 +1,641 @@ +//! TODO + +#![allow(unused)] + +use darling::{FromAttributes, FromDeriveInput}; +use iroha_macro_utils::Emitter; +use manyhow::{manyhow, Result}; +use proc_macro2::TokenStream; + +use crate::ast::Input; + +/// Derive `iroha_config_base::reader::ReadConfig` trait. +/// +/// Example: +/// +/// ``` +/// use iroha_config_base_derive::ReadConfig; +/// +/// #[derive(ReadConfig)] +/// struct Config { +/// #[config(default, env = "FOO")] +/// foo: bool, +/// #[config(nested)] +/// nested: Nested, +/// } +/// +/// #[derive(ReadConfig)] +/// struct Nested { +/// #[config(default = "42")] +/// foo: u64, +/// } +/// ``` +/// +/// Supported field attributes: +/// +/// - `env = ""` - read parameter from env (bound: `T: FromEnvStr`) +/// - `default` - fallback to default value (bound: `T: Default`) +/// - `default = ""` - fallback to a default value specified as an expression +/// - `nested` - delegates further reading (bound: `T: ReadConfig`). +/// It uses the field name as a namespace. Conflicts with others. +/// +/// Supported field shapes (if `nested` is not specified): +/// +/// - `T` - required parameter +/// - `WithOrigin` - required parameter with origin data +/// - `Option` - optional parameter +/// - `Option>` - optional parameter with origin data +/// +/// Note: the macro recognizes the shape **syntactically**. That is, the wrapper types must appear +/// with exactly these names, with optional paths to them +/// (e.g. `Option` is equivalent to `std::option::Option`). +/// +/// If `default` attr is specified, it expects a default value for the actual field type, i.e. `T`. +/// +/// Note: bounds aren't generated by this macro, +/// but you will get compile errors if you do not satisfy them. +#[manyhow] +#[proc_macro_derive(ReadConfig, attributes(config))] +pub fn derive_read_config(input: TokenStream) -> TokenStream { + let mut emitter = Emitter::new(); + + let Some(input) = emitter.handle(syn::parse2(input)) else { + return emitter.finish_token_stream(); + }; + let Some(parsed) = emitter.handle(Input::from_derive_input(&input)) else { + return emitter.finish_token_stream(); + }; + let Some(ir) = parsed.lower(&mut emitter) else { + return emitter.finish_token_stream(); + }; + + emitter.finish_token_stream_with(ir.generate()) +} + +/// Parsing proc-macro input +mod ast { + use std::collections::HashSet; + + use iroha_macro_utils::Emitter; + use manyhow::{emit, JoinToTokensError}; + use proc_macro2::{Ident, Span, TokenStream, TokenTree}; + use syn::{parse::ParseStream, punctuated::Punctuated, Token}; + + use crate::codegen; + + // TODO: `attributes(config)` rejects all unknown fields + // it would be better to emit an error "we don't support struct attrs" instead + #[derive(darling::FromDeriveInput, Debug)] + #[darling(supports(struct_named), attributes(config))] + pub struct Input { + ident: syn::Ident, + generics: syn::Generics, + data: darling::ast::Data<(), Field>, + } + + impl Input { + pub fn lower(self, emitter: &mut Emitter) -> Option { + let mut halt_codegen = false; + + for i in self.generics.params { + emit!( + emitter, + i, + "[derive(ReadConfig)]: generics are not supported" + ); + // proceeding to codegen with these errors will produce a mess + halt_codegen = true; + } + + let entries = self + .data + .take_struct() + .expect("darling should reject enums") + .fields + .into_iter() + .map(|field| field.into_codegen(emitter)) + .collect(); + + if halt_codegen { + None + } else { + Some(codegen::Ir { + ident: self.ident, + entries, + }) + } + } + } + + #[derive(Debug)] + struct Field { + ident: syn::Ident, + ty: syn::Type, + attrs: Attrs, + } + + impl darling::FromField for Field { + fn from_field(field: &syn::Field) -> darling::Result { + let ident = field + .ident + .as_ref() + .expect("darling should only allow named structs") + .clone(); + let ty = field.ty.clone(); + + let attrs: Attrs = + iroha_macro_utils::parse_single_list_attr_opt("config", &field.attrs)? + .unwrap_or_default(); + + Ok(Self { ident, ty, attrs }) + } + } + + impl Field { + fn into_codegen(self, emitter: &mut Emitter) -> codegen::Entry { + let Field { ident, ty, attrs } = self; + + let kind = match attrs { + Attrs::Nested => codegen::EntryKind::Nested, + Attrs::Parameter { default, env } => { + let shape = ParameterTypeShape::analyze(&ty); + let evaluation = match (shape.option, default) { + (false, None) => codegen::Evaluation::Required, + (false, Some(AttrDefault::Value(expr))) => { + codegen::Evaluation::OrElse(expr) + } + (false, Some(AttrDefault::Flag)) => codegen::Evaluation::OrDefault, + (true, None) => codegen::Evaluation::Optional, + (true, _) => { + emit!(emitter, ident, "parameter of type `Option<..>` conflicts with `config(default)` attribute"); + codegen::Evaluation::Optional + } + }; + + codegen::EntryKind::Parameter { + env, + evaluation, + with_origin: shape.with_origin, + } + } + }; + + codegen::Entry { ident, kind } + } + } + + #[derive(Debug)] + enum Attrs { + Nested, + Parameter { + default: Option, + env: Option, + }, + } + + impl Default for Attrs { + fn default() -> Self { + Self::Parameter { + default: <_>::default(), + env: <_>::default(), + } + } + } + + #[derive(Debug)] + enum AttrDefault { + /// `config(default)` + Flag, + /// `config(default = "")` + Value(syn::Expr), + } + + impl syn::parse::Parse for Attrs { + #[allow(clippy::too_many_lines)] + fn parse(input: ParseStream) -> syn::Result { + #[derive(Default)] + struct Accumulator { + default: Option, + env: Option<(Span, syn::LitStr)>, + nested: Option, + } + + fn reject_duplicate( + acc: &mut Option, + span: Span, + value: T, + ) -> Result<(), syn::Error> { + if acc.is_some() { + Err(syn::Error::new(span, "duplicate attribute")) + } else { + *acc = Some(value); + Ok(()) + } + } + + let mut acc = Accumulator::default(); + let tokens: Punctuated = Punctuated::parse_terminated(input)?; + for token in tokens { + match token { + AttrItem::Default(span, value) => { + reject_duplicate(&mut acc.default, span, value)? + } + AttrItem::Env(span, value) => { + reject_duplicate(&mut acc.env, span, (span, value))? + } + AttrItem::Nested(span) => reject_duplicate(&mut acc.nested, span, span)?, + } + } + + let value = match acc { + Accumulator { + nested: Some(_), + default: None, + env: None, + } => Self::Nested, + Accumulator { + nested: Some(span), .. + } => { + return Err(syn::Error::new( + span, + "attributes conflict: `nested` cannot be set with other attributes", + )) + } + Accumulator { default, env, .. } => Self::Parameter { + default, + env: env.map(|(_, lit)| lit), + }, + }; + + Ok(value) + } + } + + #[derive(Debug)] + /// A single item in the attribute list. Used for parsing of [`Attrs`]. + enum AttrItem { + Default(Span, AttrDefault), + Env(Span, syn::LitStr), + Nested(Span), + } + + impl syn::parse::Parse for AttrItem { + fn parse(input: ParseStream) -> syn::Result { + input.step(|cursor| { + const EXPECTED_IDENT: &str = + "unexpected token; expected `default`, `env`, or `nested`"; + + let Some((ident, cursor)) = cursor.ident() else { + Err(syn::Error::new(cursor.span(), EXPECTED_IDENT))? + }; + + match ident.to_string().as_str() { + "default" => { + let (lit_str, cursor) = expect_eq_with_lit_str(cursor)?; + let Some(lit_str) = lit_str else { + return Ok((Self::Default(ident.span(), AttrDefault::Flag), cursor)); + }; + let expr: syn::Expr = lit_str.parse().map_err(|err| { + syn::Error::new(err.span(), format!("expected a valid expression within `default = \"\"`, but couldn't parse it: {err}")) + })?; + Ok((Self::Default(ident.span(), AttrDefault::Value(expr)), cursor)) + } + "nested" => { + Ok((Self::Nested(ident.span()), cursor)) + } + "env" => { + let (Some(lit), cursor) = expect_eq_with_lit_str(cursor)? else { + return Err(syn::Error::new( + ident.span(), + "expected `env` to be set as `env = \"VARIABLE_NAME\"", + )); + }; + Ok((Self::Env(ident.span(), lit), cursor)) + } + other => Err(syn::Error::new(cursor.span(), EXPECTED_IDENT)), + } + }) + } + } + + fn expect_eq_with_lit_str( + cursor: syn::buffer::Cursor, + ) -> syn::Result<(Option, syn::buffer::Cursor)> { + const EXPECTED_STR_LIT: &str = r#"expected a string literal, e.g. "...""#; + + let next = match cursor.token_tree() { + Some((TokenTree::Punct(punct), next)) if punct.as_char() == '=' => next, + _ => return Ok((None, cursor)), + }; + + let (lit, next) = match next.token_tree() { + Some((TokenTree::Literal(lit), next)) => (lit, next), + Some((other, _)) => Err(syn::Error::new(other.span(), EXPECTED_STR_LIT))?, + None => Err(syn::Error::new(next.span(), EXPECTED_STR_LIT))?, + }; + + let string = lit.to_string(); + let trimmed = string.trim_matches('"'); + if string == trimmed { + // not a string literal + Err(syn::Error::new(lit.span(), EXPECTED_STR_LIT))?; + } + + let lit_str = syn::LitStr::new(trimmed, lit.span()); + Ok((Some(lit_str), next)) + } + + #[derive(Debug, PartialEq)] + struct ParameterTypeShape { + option: bool, + with_origin: bool, + } + + impl ParameterTypeShape { + fn analyze(ty: &syn::Type) -> Self { + #[derive(Debug)] + enum Token { + Option, + WithOrigin, + Unknown, + } + + fn try_find(ty: &syn::Type) -> Option<(Option<&syn::Type>, &Ident)> { + if let syn::Type::Path(type_path) = ty { + if let Some(last_segment) = type_path.path.segments.last() { + match &last_segment.arguments { + syn::PathArguments::AngleBracketed(args) if args.args.len() == 1 => { + if let syn::GenericArgument::Type(ty) = + args.args.first().expect("should be exactly 1") + { + return Some((Some(ty), &last_segment.ident)); + } + } + syn::PathArguments::None => return Some((None, &last_segment.ident)), + _ => {} + } + } + } + + None + } + + fn parse_tokens(ty: &syn::Type, depth: u8) -> Vec { + if depth == 0 { + return vec![]; + } + + if let Some((next, ident)) = try_find(ty) { + let token = match ident.to_string().as_ref() { + "Option" => Token::Option, + "WithOrigin" => Token::WithOrigin, + _ => Token::Unknown, + }; + + let mut chain = vec![token]; + + if let Some(next) = next { + chain.extend(parse_tokens(next, depth - 1)); + } + chain + } else { + vec![] + } + } + + let chain = parse_tokens(ty, 3); + + let (option, with_origin) = match (chain.first(), chain.get(1)) { + (Some(Token::Option), Some(Token::WithOrigin)) => (true, true), + (Some(Token::Option), Some(_)) => (true, false), + (Some(Token::WithOrigin), _) => (false, true), + _ => (false, false), + }; + + Self { + option, + with_origin, + } + } + } + + #[cfg(test)] + mod tests { + use syn::parse_quote; + + use super::*; + + #[test] + fn parse_default() { + let attrs: Attrs = syn::parse_quote!(default); + + assert!(matches!( + attrs, + Attrs::Parameter { + default: Some(AttrDefault::Flag), + env: None + } + )); + } + + #[test] + fn parse_default_with_expr() { + let attrs: Attrs = syn::parse_quote!(default = "42 + 411"); + + assert!(matches!( + attrs, + Attrs::Parameter { + default: Some(AttrDefault::Value(_)), + env: None + } + )); + } + + #[test] + fn parse_default_env() { + let attrs: Attrs = syn::parse_quote!(default, env = "$!@#"); + + let Attrs::Parameter { + default: Some(AttrDefault::Flag), + env: Some(var), + } = attrs + else { + panic!("expectation failed") + }; + assert_eq!(var.value(), "$!@#"); + } + + #[test] + #[should_panic( + expected = "attributes conflict: `nested` cannot be set with other attributes" + )] + fn conflict() { + let _: Attrs = syn::parse_quote!(nested, default); + } + + #[test] + #[should_panic(expected = "duplicate attribute")] + fn duplicates() { + let _: Attrs = syn::parse_quote!(default, default); + } + + #[test] + fn determine_shapes() { + macro_rules! case { + ($input:ty, $option:literal, $with_origin:literal) => { + let ty: syn::Type = syn::parse_quote!($input); + let shape = ParameterTypeShape::analyze(&ty); + assert_eq!( + shape, + ParameterTypeShape { + option: $option, + with_origin: $with_origin + } + ); + }; + } + + case!(Something, false, false); + case!(Option, true, false); + case!(Option>, true, true); + case!(WithOrigin, false, true); + case!(WithOrigin>, false, true); + case!(Option>>, true, false); + case!( + std::option::Option>, + true, + true + ); + } + } +} + +/// Generating code based on [`model`] +mod codegen { + use proc_macro2::TokenStream; + use quote::quote; + + pub struct Ir { + /// The type we are implementing `ReadConfig` for + pub ident: syn::Ident, + pub entries: Vec, + } + + impl Ir { + pub fn generate(self) -> TokenStream { + let (read_fields, unwrap_fields): (Vec<_>, Vec<_>) = self + .entries + .into_iter() + .map(Entry::generate) + .map(|EntryParts { read, unwrap }| (read, unwrap)) + .unzip(); + + let ident = self.ident; + + quote! { + impl ::iroha_config_base::read::ReadConfig for #ident { + fn read( + __reader: &mut ::iroha_config_base::read::ConfigReader + ) -> ::iroha_config_base::read::FinalWrap { + #(#read_fields)* + + ::iroha_config_base::read::FinalWrap::value_fn(|| Self { + #(#unwrap_fields),* + }) + } + } + } + } + } + + pub struct Entry { + pub ident: syn::Ident, + pub kind: EntryKind, + } + + impl Entry { + fn generate(self) -> EntryParts { + let Self { kind, ident } = self; + + let read = match kind { + EntryKind::Nested => { + quote! { let #ident = __reader.read_nested(stringify!(#ident)); } + } + EntryKind::Parameter { + env, + evaluation, + with_origin, + } => { + let mut read = quote! { + let #ident = __reader.read_parameter([stringify!(#ident)]) + }; + if let Some(var) = env { + read.extend(quote! { .env(#var) }) + } + read.extend(match evaluation { + Evaluation::Required => quote! { .value_required() }, + Evaluation::OrElse(expr) => quote! { .value_or_else(|| #expr) }, + Evaluation::OrDefault => quote! { .value_or_default() }, + Evaluation::Optional => quote! { .value_optional() }, + }); + read.extend(if with_origin { + quote! { .finish_with_origin(); } + } else { + quote! { .finish(); } + }); + read + } + }; + + EntryParts { + read, + unwrap: quote! { #ident: #ident.unwrap() }, + } + } + } + + struct EntryParts { + read: TokenStream, + unwrap: TokenStream, + } + + pub enum EntryKind { + Parameter { + env: Option, + evaluation: Evaluation, + with_origin: bool, + }, + Nested, + } + + pub enum Evaluation { + Required, + OrElse(syn::Expr), + OrDefault, + Optional, + } + + #[cfg(test)] + mod tests { + use expect_test::expect; + use syn::parse_quote; + + use super::*; + + #[test] + fn entry_with_env_reading() { + let entry = Entry { + ident: parse_quote!(test), + kind: EntryKind::Parameter { + env: Some(parse_quote!("TEST_ENV")), + evaluation: Evaluation::Required, + with_origin: false, + }, + }; + + let actual = entry.generate().read.to_string(); + + expect![[r#"let test = __reader . read_parameter ([stringify ! (test)]) . env ("TEST_ENV") . value_required () . finish () ;"#]].assert_eq(&actual); + } + } +} diff --git a/config/base/derive/tests/ui.rs b/config/base/derive/tests/ui.rs new file mode 100644 index 00000000000..f85c931983c --- /dev/null +++ b/config/base/derive/tests/ui.rs @@ -0,0 +1,9 @@ +#![cfg(not(coverage))] +use trybuild::TestCases; + +#[test] +fn ui() { + let test_cases = TestCases::new(); + test_cases.pass("tests/ui_pass/*.rs"); + test_cases.compile_fail("tests/ui_fail/*.rs"); +} diff --git a/config/base/derive/tests/ui_fail/generics.rs b/config/base/derive/tests/ui_fail/generics.rs new file mode 100644 index 00000000000..4eddc66b7dd --- /dev/null +++ b/config/base/derive/tests/ui_fail/generics.rs @@ -0,0 +1,8 @@ +use iroha_config_base::ReadConfig; + +#[derive(ReadConfig)] +struct Test { + foo: T, +} + +pub fn main() {} diff --git a/config/base/derive/tests/ui_fail/generics.stderr b/config/base/derive/tests/ui_fail/generics.stderr new file mode 100644 index 00000000000..44fb59fb387 --- /dev/null +++ b/config/base/derive/tests/ui_fail/generics.stderr @@ -0,0 +1,5 @@ +error: [derive(ReadConfig)]: generics are not supported + --> tests/ui_fail/generics.rs:4:13 + | +4 | struct Test { + | ^ diff --git a/config/base/derive/tests/ui_fail/invalid_attrs_commas.rs b/config/base/derive/tests/ui_fail/invalid_attrs_commas.rs new file mode 100644 index 00000000000..bb29560e0b5 --- /dev/null +++ b/config/base/derive/tests/ui_fail/invalid_attrs_commas.rs @@ -0,0 +1,9 @@ +use iroha_config_base::ReadConfig; + +#[derive(ReadConfig)] +struct Test { + #[config(,,,)] + foo: u64, +} + +pub fn main() {} diff --git a/config/base/derive/tests/ui_fail/invalid_attrs_commas.stderr b/config/base/derive/tests/ui_fail/invalid_attrs_commas.stderr new file mode 100644 index 00000000000..b8f3e3ccfa8 --- /dev/null +++ b/config/base/derive/tests/ui_fail/invalid_attrs_commas.stderr @@ -0,0 +1,5 @@ +error: unexpected token; expected `default`, `env`, or `nested` + --> tests/ui_fail/invalid_attrs_commas.rs:5:14 + | +5 | #[config(,,,)] + | ^ diff --git a/config/base/derive/tests/ui_fail/invalid_attrs_conflicts.rs b/config/base/derive/tests/ui_fail/invalid_attrs_conflicts.rs new file mode 100644 index 00000000000..65715f5130a --- /dev/null +++ b/config/base/derive/tests/ui_fail/invalid_attrs_conflicts.rs @@ -0,0 +1,15 @@ +use iroha_config_base::ReadConfig; + +#[derive(ReadConfig)] +struct Test { + #[config(nested, default)] + foo: u64, +} + +#[derive(ReadConfig)] +struct Test2 { + #[config(default, nested)] + foo: u64, +} + +pub fn main() {} diff --git a/config/base/derive/tests/ui_fail/invalid_attrs_conflicts.stderr b/config/base/derive/tests/ui_fail/invalid_attrs_conflicts.stderr new file mode 100644 index 00000000000..57534b4f6b6 --- /dev/null +++ b/config/base/derive/tests/ui_fail/invalid_attrs_conflicts.stderr @@ -0,0 +1,11 @@ +error: attributes conflict: `nested` cannot be set with other attributes + --> tests/ui_fail/invalid_attrs_conflicts.rs:5:14 + | +5 | #[config(nested, default)] + | ^^^^^^ + +error: attributes conflict: `nested` cannot be set with other attributes + --> tests/ui_fail/invalid_attrs_conflicts.rs:11:23 + | +11 | #[config(default, nested)] + | ^^^^^^ diff --git a/config/base/derive/tests/ui_fail/invalid_attrs_default_invalid_expr.rs b/config/base/derive/tests/ui_fail/invalid_attrs_default_invalid_expr.rs new file mode 100644 index 00000000000..ad7e360fa58 --- /dev/null +++ b/config/base/derive/tests/ui_fail/invalid_attrs_default_invalid_expr.rs @@ -0,0 +1,9 @@ +use iroha_config_base::ReadConfig; + +#[derive(ReadConfig)] +struct Test { + #[config(default = "not an expression I guess")] + foo: u64, +} + +pub fn main() {} diff --git a/config/base/derive/tests/ui_fail/invalid_attrs_default_invalid_expr.stderr b/config/base/derive/tests/ui_fail/invalid_attrs_default_invalid_expr.stderr new file mode 100644 index 00000000000..79099e9591b --- /dev/null +++ b/config/base/derive/tests/ui_fail/invalid_attrs_default_invalid_expr.stderr @@ -0,0 +1,5 @@ +error: expected a valid expression within `default = ""`, but couldn't parse it: unexpected token + --> tests/ui_fail/invalid_attrs_default_invalid_expr.rs:5:24 + | +5 | #[config(default = "not an expression I guess")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/config/base/derive/tests/ui_fail/invalid_attrs_env_without_var.rs b/config/base/derive/tests/ui_fail/invalid_attrs_env_without_var.rs new file mode 100644 index 00000000000..e8f8c22e6b6 --- /dev/null +++ b/config/base/derive/tests/ui_fail/invalid_attrs_env_without_var.rs @@ -0,0 +1,9 @@ +use iroha_config_base::ReadConfig; + +#[derive(ReadConfig)] +struct Test { + #[config(env)] // without `= "VAR"` + foo: u64, +} + +pub fn main() {} diff --git a/config/base/derive/tests/ui_fail/invalid_attrs_env_without_var.stderr b/config/base/derive/tests/ui_fail/invalid_attrs_env_without_var.stderr new file mode 100644 index 00000000000..53c3c4350d1 --- /dev/null +++ b/config/base/derive/tests/ui_fail/invalid_attrs_env_without_var.stderr @@ -0,0 +1,5 @@ +error: expected `env` to be set as `env = "VARIABLE_NAME" + --> tests/ui_fail/invalid_attrs_env_without_var.rs:5:14 + | +5 | #[config(env)] // without `= "VAR"` + | ^^^ diff --git a/config/base/derive/tests/ui_fail/invalid_attrs_no_comma_between_attrs.rs b/config/base/derive/tests/ui_fail/invalid_attrs_no_comma_between_attrs.rs new file mode 100644 index 00000000000..4fd9a7f7c8a --- /dev/null +++ b/config/base/derive/tests/ui_fail/invalid_attrs_no_comma_between_attrs.rs @@ -0,0 +1,9 @@ +use iroha_config_base::ReadConfig; + +#[derive(ReadConfig)] +struct Test { + #[config(default env = "1234")] + foo: u64, +} + +pub fn main() {} diff --git a/config/base/derive/tests/ui_fail/invalid_attrs_no_comma_between_attrs.stderr b/config/base/derive/tests/ui_fail/invalid_attrs_no_comma_between_attrs.stderr new file mode 100644 index 00000000000..72636f9ee1e --- /dev/null +++ b/config/base/derive/tests/ui_fail/invalid_attrs_no_comma_between_attrs.stderr @@ -0,0 +1,5 @@ +error: expected `,` + --> tests/ui_fail/invalid_attrs_no_comma_between_attrs.rs:5:22 + | +5 | #[config(default env = "1234")] + | ^^^ diff --git a/config/base/derive/tests/ui_fail/invalid_attrs_struct.rs b/config/base/derive/tests/ui_fail/invalid_attrs_struct.rs new file mode 100644 index 00000000000..bf01ab9fd8e --- /dev/null +++ b/config/base/derive/tests/ui_fail/invalid_attrs_struct.rs @@ -0,0 +1,9 @@ +use iroha_config_base::ReadConfig; + +#[derive(ReadConfig)] +#[config(whatever)] // not supported on structs +struct Test1 { + foo: u64, +} + +pub fn main() {} diff --git a/config/base/derive/tests/ui_fail/invalid_attrs_struct.stderr b/config/base/derive/tests/ui_fail/invalid_attrs_struct.stderr new file mode 100644 index 00000000000..1a152a37be4 --- /dev/null +++ b/config/base/derive/tests/ui_fail/invalid_attrs_struct.stderr @@ -0,0 +1,5 @@ +error: Unknown field: `whatever` + --> tests/ui_fail/invalid_attrs_struct.rs:4:10 + | +4 | #[config(whatever)] // not supported on structs + | ^^^^^^^^ diff --git a/config/base/derive/tests/ui_fail/unsupported_shapes.rs b/config/base/derive/tests/ui_fail/unsupported_shapes.rs new file mode 100644 index 00000000000..b3cc9ec719b --- /dev/null +++ b/config/base/derive/tests/ui_fail/unsupported_shapes.rs @@ -0,0 +1,17 @@ +use iroha_config_base::ReadConfig; + +#[derive(ReadConfig)] +struct Test1; + +#[derive(ReadConfig)] +struct Test2(u64); // newtype + +#[derive(ReadConfig)] +struct Test3(u64, u32); // unnamed + +#[derive(ReadConfig)] +enum Test4 { + One, +} + +pub fn main() {} diff --git a/config/base/derive/tests/ui_fail/unsupported_shapes.stderr b/config/base/derive/tests/ui_fail/unsupported_shapes.stderr new file mode 100644 index 00000000000..abcd9dc5df9 --- /dev/null +++ b/config/base/derive/tests/ui_fail/unsupported_shapes.stderr @@ -0,0 +1,31 @@ +error: Unsupported shape `no fields`. Expected named fields. + --> tests/ui_fail/unsupported_shapes.rs:3:10 + | +3 | #[derive(ReadConfig)] + | ^^^^^^^^^^ + | + = note: this error originates in the derive macro `ReadConfig` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: Unsupported shape `one unnamed field`. Expected named fields. + --> tests/ui_fail/unsupported_shapes.rs:6:10 + | +6 | #[derive(ReadConfig)] + | ^^^^^^^^^^ + | + = note: this error originates in the derive macro `ReadConfig` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: Unsupported shape `unnamed fields`. Expected named fields. + --> tests/ui_fail/unsupported_shapes.rs:9:10 + | +9 | #[derive(ReadConfig)] + | ^^^^^^^^^^ + | + = note: this error originates in the derive macro `ReadConfig` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: Unsupported shape `enum`. Expected struct with named fields. + --> tests/ui_fail/unsupported_shapes.rs:12:10 + | +12 | #[derive(ReadConfig)] + | ^^^^^^^^^^ + | + = note: this error originates in the derive macro `ReadConfig` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/config/base/derive/tests/ui_pass/happy_path.rs b/config/base/derive/tests/ui_pass/happy_path.rs new file mode 100644 index 00000000000..27b4b8efd8d --- /dev/null +++ b/config/base/derive/tests/ui_pass/happy_path.rs @@ -0,0 +1,26 @@ +use iroha_config_base::{ReadConfig, WithOrigin}; + +#[derive(ReadConfig)] +struct Test { + required: u64, + required_with_origin: WithOrigin, + optional: Option, + optional_with_origin: Option>, + #[config(default)] + with_default: bool, + #[config(default = "true")] + with_default_expr: bool, + #[config(env = "FROM_ENV")] + from_env: String, + #[config(nested)] + nested: Nested, + #[config(env = "TEST", default = "true")] + with_default_expr_and_env: bool, +} + +#[derive(ReadConfig)] +struct Nested { + foo: Option, +} + +pub fn main() {} diff --git a/config/base/src/attach.rs b/config/base/src/attach.rs new file mode 100644 index 00000000000..8117bb8a419 --- /dev/null +++ b/config/base/src/attach.rs @@ -0,0 +1,233 @@ +//! Various attachments for [`error_stack::Report::attach`] API. +// TODO: use `error_stack` hooks to enhance attachments with colors +// TODO: standardize more attachments used in `read.rs` + +use std::{ + fmt::{Debug, Display, Formatter}, + marker::PhantomData, + path::{Path, PathBuf}, +}; + +use derive_more::{Constructor, Display}; + +use crate::ParameterOrigin; + +/// Attach a file path +#[derive(Constructor, Display, Debug)] +#[display(fmt = "file path: {}", "path.display()")] +pub struct FilePath { + path: PathBuf, +} + +/// Attach an actual value +#[derive(Constructor, Display, Debug)] +#[display(fmt = "actual value: {value}")] +pub struct ActualValue +where + T: Display + Debug, +{ + value: T, +} + +/// Attach an expectation +#[derive(Constructor, Display, Debug)] +#[display(fmt = "expected: {message}")] +pub struct Expected +where + T: Display + Debug, +{ + message: T, +} + +/// Attach a chain of extensions (see [`crate::util::ExtendsPaths`]) +#[derive(Constructor, Display, Debug)] +#[display( + fmt = "extending ({depth}): `{}` -> `{}`", + "from.display()", + "to.display()" +)] +pub struct ExtendsChain { + from: PathBuf, + to: PathBuf, + depth: u8, +} + +/// Attach an environment key-value entry +#[derive(Constructor, Display, Debug)] +#[display(fmt = "value: {var}={value}")] +pub struct EnvValue { + var: String, + value: String, +} + +/// Attach config value and its origin. +/// +/// Usually constructed via [`crate::WithOrigin::into_attachment`]. +/// +/// To support displaying values which don't implement [Display], it uses formats mechanism. +/// For example: +/// +/// - For [Path], use [`ConfigValueAndOrigin::display_path`] +/// - For [Debug], use [`ConfigValueAndOrigin::display_as_debug`] +/// +/// Example usage with a path: +/// +/// ``` +/// use std::path::PathBuf; +/// use error_stack::Report; +/// use iroha_config_base::{ParameterOrigin, WithOrigin}; +/// +/// let value = PathBuf::from("/path/to/somewhere"); +/// let attachment = WithOrigin::new( +/// value, +/// ParameterOrigin::file( +/// ["a", "b"].into(), +/// PathBuf::from("/root/iroha/config.toml") +/// ) +/// ) +/// .into_attachment() +/// .display_path(); +/// +/// assert_eq!( +/// format!("{attachment}"), +/// "config origin: parameter `a.b` with value `/path/to/somewhere` in file `/root/iroha/config.toml`" +/// ); +/// +/// let _report = Report::new(std::io::Error::other("test")).attach(attachment); +/// ``` +pub struct ConfigValueAndOrigin> { + value: T, + origin: ParameterOrigin, + _f: PhantomData, +} + +impl Debug for ConfigValueAndOrigin +where + T: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ConfigValueAndOrigin") + .field("value", &self.value) + .field("origin", &self.origin) + .finish() + } +} + +impl ConfigValueAndOrigin { + fn new_internal(value: T, origin: ParameterOrigin) -> Self { + Self { + value, + origin, + _f: PhantomData, + } + } +} + +impl ConfigValueAndOrigin { + /// Constructor + pub fn new(value: T, origin: ParameterOrigin) -> Self { + ConfigValueAndOrigin::new_internal(value, origin) + } +} + +impl> ConfigValueAndOrigin { + /// Switch to [`FormatPath`] + pub fn display_path(self) -> ConfigValueAndOrigin> { + ConfigValueAndOrigin::new_internal(self.value, self.origin) + } +} + +impl ConfigValueAndOrigin { + /// Switch to [`FormatDebug`] + pub fn display_as_debug(self) -> ConfigValueAndOrigin> { + ConfigValueAndOrigin::new_internal(self.value, self.origin) + } +} + +/// Workaround that [`ConfigValueAndOrigin`] uses to display a value that doesn't +/// implement [`Display`] directly using some format, e.g. [`FormatPath`]. +pub trait DisplayProxy { + /// Associated type for which the implementor is proxying [`Display`] implementation. + type Base: ?Sized; + + /// Similar to [`Display::fmt`], but uses an associated type instead of `self`. + #[allow(clippy::missing_errors_doc)] + fn fmt(value: &Self::Base, f: &mut Formatter<'_>) -> std::fmt::Result; +} + +/// Indicates formating of a value that implements [`Display`]. +pub struct FormatDisplay(PhantomData); + +impl DisplayProxy for FormatDisplay { + type Base = T; + + fn fmt(value: &Self::Base, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{value}") + } +} + +/// Indicates formatting of a [`Path`] using [`Path::display`]. +#[allow(missing_copy_implementations)] +pub struct FormatPath(PhantomData); + +impl> DisplayProxy for FormatPath { + type Base = T; + + fn fmt(value: &Self::Base, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", value.as_ref().display()) + } +} + +/// Indicates formatting using [`Debug`]. +pub struct FormatDebug(PhantomData); + +impl DisplayProxy for FormatDebug { + type Base = T; + + fn fmt(value: &Self::Base, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{value:?}") + } +} + +struct DisplayWithProxy<'a, T, F>(&'a T, PhantomData); + +impl Display for DisplayWithProxy<'_, T, F> +where + F: DisplayProxy, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + ::fmt(self.0, f) + } +} + +impl Display for ConfigValueAndOrigin +where + F: DisplayProxy, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let Self { origin, value, .. } = &self; + let value = DisplayWithProxy(value, PhantomData::); + + write!(f, "config origin: ")?; + + match origin { + ParameterOrigin::File { id, path } => write!( + f, + "parameter `{id}` with value `{value}` in file `{}`", + path.display() + ), + ParameterOrigin::Env { id, var } => write!( + f, + "parameter `{id}` with value `{value}` set from environment variable `{var}`" + ), + ParameterOrigin::EnvUnknown { id } => write!( + f, + "parameter `{id}` with value `{value}` set from environment variables" + ), + ParameterOrigin::Default { id } => { + write!(f, "parameter `{id}` with default value `{value}`") + } + ParameterOrigin::Custom { message } => write!(f, "{message}"), + } + } +} diff --git a/config/base/src/env.rs b/config/base/src/env.rs new file mode 100644 index 00000000000..c8f218311f9 --- /dev/null +++ b/config/base/src/env.rs @@ -0,0 +1,138 @@ +//! Environment variables + +use std::{ + borrow::Cow, + cell::RefCell, + collections::{HashMap, HashSet}, + ops::Sub, + rc::Rc, + str::FromStr, +}; + +use error_stack::Context; + +/// Convertation from a string read from an environment variable to a specific value. +/// +/// Has an implementation for any type that implements [`FromStr`] (with an error that is [Context]). +pub trait FromEnvStr { + /// Error that might occur during conversion + type Error: Context; + + /// The conversion itself. + /// + /// # Errors + /// Up to an implementor. + fn from_env_str(value: Cow<'_, str>) -> Result + where + Self: Sized; +} + +impl FromEnvStr for T +where + T: FromStr, + ::Err: Context, +{ + type Error = ::Err; + + fn from_env_str(value: Cow<'_, str>) -> Result + where + Self: Sized, + { + Self::from_str(&value) + } +} + +/// Enables polymorphism for environment readers. +/// Has default implementations for plain functions, +/// thus it should work for closures as well. +pub trait ReadEnv { + /// Read a value from an environment variable. + fn read_env(&self, key: &str) -> Option>; +} + +impl ReadEnv for F +where + F: Fn(&str) -> Option>, +{ + fn read_env(&self, key: &str) -> Option> { + self(key) + } +} + +/// An adapter of [`std::env::var`] for [`ReadEnv`] trait. +/// Does not fail in case of [`std::env::VarError::NotUnicode`], but prints it as an error via +/// [log]. +/// +/// [`crate::read::ConfigReader`] uses it by default. +pub fn std_env(key: &str) -> Option> { + match std::env::var(key) { + Ok(value) => Some(Cow::from(value)), + Err(std::env::VarError::NotPresent) => None, + Err(_) => { + log::error!( + "Found non-unicode characters in env var `{}`, ignoring", + key + ); + None + } + } +} + +/// An implementation of [`ReadEnv`] for testing convenience. +#[derive(Default, Clone)] +pub struct MockEnv { + map: HashMap, + visited: Rc>>, +} + +impl MockEnv { + /// Create new empty environment + pub fn new() -> Self { + Self::default() + } + + /// Create an environment with a given map + pub fn with_map(map: HashMap) -> Self { + Self { map, ..Self::new() } + } + + /// Get a set of keys not visited yet by [`ReadEnv::read_env`] + /// + /// Since [`Rc`] is used under the hood, should work on clones as well. + pub fn unvisited(&self) -> HashSet { + self.known_keys().sub(&*self.visited.borrow()) + } + + /// Similar to [`Self::unvisited`], but gives requested entries + /// that don't exist within the set of variables + pub fn unknown(&self) -> HashSet { + self.visited.borrow().sub(&self.known_keys()) + } + + fn known_keys(&self) -> HashSet { + self.map.keys().map(ToOwned::to_owned).collect() + } +} + +impl From for MockEnv +where + T: IntoIterator, + K: AsRef, + V: AsRef, +{ + fn from(value: T) -> Self { + Self::with_map( + value + .into_iter() + .map(|(k, v)| (k.as_ref().to_string(), v.as_ref().to_string())) + .collect(), + ) + } +} + +impl ReadEnv for MockEnv { + fn read_env(&self, key: &str) -> Option> { + self.visited.borrow_mut().insert(key.to_string()); + self.map.get(key).map(Cow::from) + } +} diff --git a/config/base/src/lib.rs b/config/base/src/lib.rs index c80b77c696e..bba7016a0a4 100644 --- a/config/base/src/lib.rs +++ b/config/base/src/lib.rs @@ -1,701 +1,328 @@ -//! Utilities behind Iroha configurations +//! Tools to work with file- and environment-based configuration. +//! +//! The main tool here is [`read::ConfigReader`]. +//! It is built around these key concepts: +//! +//! - Read config from TOML files; +//! - Identify each configuration parameter by its path in the file; +//! - Parameter might have an environment variable alias, which overwrites value from files; +//! - Parameter might have a default value, applied if nothing was found in files/env. +//! +//! The reader's goal is to: +//! +//! - Give an exhaustive error report if something fails; +//! - Give origins of values for later use in error reports (see [`WithOrigin`]); +//! - Gives traces for debugging purposes (by [`log`] crate). +//! +//! ## Example: raw usage +//! +//! Let's say we want to read the following config: +//! +//! ```toml +//! [foo] +//! bar = "example" # has env alias BAR +//! baz = 42 +//! more = { foo = 24 } +//! ``` +//! +//! The reading and manual implementation of [`read::ReadConfig`] might look like: +//! +//! ``` +//! use iroha_config_base::{ +//! read::{ConfigReader, FinalWrap, ReadConfig}, +//! toml::TomlSource, +//! WithOrigin, +//! }; +//! use serde::Deserialize; +//! use toml::toml; +//! +//! struct Config { +//! foo_bar: String, +//! foo_baz: WithOrigin, +//! more: Option, +//! } +//! +//! #[derive(Deserialize)] +//! struct More { +//! foo: u8, +//! } +//! +//! impl ReadConfig for Config { +//! fn read(reader: &mut ConfigReader) -> FinalWrap { +//! let foo_bar = reader +//! .read_parameter(["foo", "bar"]) +//! .env("BAR") +//! .value_required() +//! .finish(); +//! +//! let foo_baz = reader +//! .read_parameter(["foo", "baz"]) +//! .value_or_else(|| 100) +//! .finish_with_origin(); +//! +//! let more = reader +//! .read_parameter(["foo", "more"]) +//! .value_optional() +//! .finish(); +//! +//! FinalWrap::value_fn(|| Self { +//! foo_bar: foo_bar.unwrap(), +//! foo_baz: foo_baz.unwrap(), +//! more: more.unwrap(), +//! }) +//! } +//! } +//! +//! let _config = ConfigReader::new() +//! .with_toml_source(TomlSource::inline(toml! { +//! [foo] +//! bar = "example" +//! baz = 42 +//! more = { foo = 24 } +//! })) +//! .read_and_complete::() +//! .expect("config is valid"); +//! ``` +//! +//! ## Example: using macro +//! +//! [`iroha_config_base_derive::ReadConfig`] macro simplifies manual work. +//! The previous example might be simplified as follows: +//! +//! ``` +//! use iroha_config_base::{ +//! read::{ConfigReader, ReadConfig}, +//! toml::TomlSource, +//! ReadConfig, WithOrigin, +//! }; +//! use serde::Deserialize; +//! use toml::toml; +//! +//! #[derive(ReadConfig)] +//! struct Config { +//! #[config(nested)] +//! foo: Foo, +//! } +//! +//! #[derive(ReadConfig)] +//! struct Foo { +//! #[config(env = "BAR")] +//! bar: String, +//! #[config(default = "100")] +//! baz: WithOrigin, +//! more: Option, +//! } +//! +//! #[derive(Deserialize)] +//! struct More { +//! foo: u8, +//! } +//! +//! let config = ConfigReader::new() +//! .with_toml_source(TomlSource::inline(toml! { +//! [foo] +//! bar = "bar" +//! })) +//! .read_and_complete::() +//! .expect("config is valid"); +//! +//! assert_eq!(config.foo.bar, "bar"); +//! assert_eq!(*config.foo.baz.value(), 100); +//! assert!(config.foo.more.is_none()); +//! ``` +//! +//! Here we also use nesting. +//! +//! See macro documentation for details. + +#![warn(missing_docs)] + +pub mod attach; +pub mod env; +pub mod read; +pub mod toml; +pub mod util; use std::{ - borrow::Cow, - cell::RefCell, - collections::{HashMap, HashSet}, - convert::Infallible, - env::VarError, - error::Error, - ffi::OsString, fmt::{Debug, Display, Formatter}, - ops::Sub, - path::PathBuf, - str::FromStr, - time::Duration, + path::{Path, PathBuf}, }; -use eyre::{eyre, Report, WrapErr}; -pub use merge::Merge; -pub use serde; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; +pub use iroha_config_base_derive::ReadConfig; -/// [`Duration`], but can parse a human-readable string. -/// TODO: currently deserializes just as [`Duration`] -#[serde_with::serde_as] -#[derive(Debug, Copy, Clone, Deserialize, Serialize, Ord, PartialOrd, Eq, PartialEq)] -pub struct HumanDuration(#[serde_as(as = "serde_with::DurationMilliSeconds")] pub Duration); +use crate::attach::ConfigValueAndOrigin; -impl HumanDuration { - /// Get the [`Duration`] - pub fn get(self) -> Duration { - self.0 - } -} - -/// Representation of amount of bytes, parseable from a human-readable string. -#[derive(Debug, Copy, Clone, Deserialize, Serialize)] -pub struct HumanBytes(pub T); - -impl HumanBytes { - /// Get the number of bytes - pub fn get(self) -> T { - self.0 - } -} - -/// Error representing a missing field in the configuration -#[derive(thiserror::Error, Debug)] -#[error("missing field: `{path}`")] -pub struct MissingFieldError { - path: String, -} - -impl MissingFieldError { - /// Create an instance - pub fn new(s: &str) -> Self { - Self { path: s.to_owned() } - } -} - -/// Provides environment variables -pub trait ReadEnv { - /// Read a value of an environment variable. - /// - /// This is a fallible operation, which might return an empty value if the given key is not - /// present. - /// - /// [`Cow`] is used for flexibility. The read value might be given both as an owned and as a - /// borrowed string depending on the structure that implements [`ReadEnv`]. On the receiving - /// part, it might be convenient to parse the string while just borrowing it - /// (e.g. with [`FromStr`]), but might be also convenient to own the value. [`Cow`] covers all - /// of this. - /// - /// # Errors - /// For any reason an implementor might have. - fn read_env(&self, key: impl AsRef) -> Result>, E>; -} - -/// Constructs from environment variables -pub trait FromEnv { - /// Constructs from environment variables using [`ReadEnv`] - /// - /// # Errors - /// For any reason an implementor might have. - // `E: Error` so that it could be wrapped into a Report - fn from_env>(env: &R) -> FromEnvResult - where - Self: Sized; -} - -/// Result of [`FromEnv::from_env`]. Intended to contain multiple possible errors at once. -pub type FromEnvResult = eyre::Result>; - -/// Marker trait to implement [`FromEnv`] if a type implements [`Default`] -pub trait FromEnvDefaultFallback {} - -impl FromEnv for T -where - T: FromEnvDefaultFallback + Default, -{ - fn from_env>(_env: &R) -> FromEnvResult - where - Self: Sized, - { - Ok(Self::default()) - } -} - -/// Simple collector of errors. +/// Config parameter ID, which is a path in config file, e.g. `foo.bar`. /// -/// Will panic on [`Drop`] if contains errors that are not handled with [`Emitter::finish`]. -pub struct Emitter { - errors: Vec, - bomb: drop_bomb::DropBomb, -} - -impl Emitter { - /// Create a new empty emitter - pub fn new() -> Self { - Self { - errors: Vec::new(), - bomb: drop_bomb::DropBomb::new( - "Errors emitter is dropped without consuming collected errors", - ), - } - } - - /// Emit a single error - pub fn emit(&mut self, error: E) { - self.errors.push(error); - } - - /// Emit a collection of errors - pub fn emit_collection(&mut self, mut errors: ErrorsCollection) { - self.errors.append(&mut errors.0); - } - - /// Transform the emitter into a [`Result`], containing an [`ErrorCollection`] if - /// any errors were emitted. - /// - /// # Errors - /// If any errors were emitted. - pub fn finish(mut self) -> Result<(), ErrorsCollection> { - self.bomb.defuse(); - - if self.errors.is_empty() { - Ok(()) - } else { - Err(ErrorsCollection(self.errors)) - } - } -} - -impl Emitter { - /// A shorthand to work with [`FromEnv`]. - /// - /// # Errors - /// If failed to parse the value from env. - pub fn try_from_env(&mut self, env: &R) -> Option - where - T: FromEnv, - R: ReadEnv, - RE: Error, - { - match FromEnv::from_env(env) { - Ok(parsed) => Some(parsed), - Err(errors) => { - self.emit_collection(errors); - None - } - } - } -} - -impl Default for Emitter { - fn default() -> Self { - Self::new() - } -} - -impl Emitter { - /// Shorthand to emit a [`MissingFieldError`]. - pub fn emit_missing_field(&mut self, field_name: impl AsRef) { - self.emit(MissingFieldError::new(field_name.as_ref())) - } - - /// Tries to [`UnwrapPartial`], collecting errors on failure. - /// - /// This method is relevant for [`Emitter`], because [`UnwrapPartial`] - /// returns a collection of [`MissingFieldError`]s. - pub fn try_unwrap_partial(&mut self, partial: P) -> Option { - partial.unwrap_partial().map_or_else( - |err| { - self.emit_collection(err); - None - }, - Some, - ) - } +/// ``` +/// use iroha_config_base::ParameterId; +/// +/// let id = ParameterId::from(["foo", "bar"]); +/// +/// assert_eq!(format!("{id}"), "foo.bar"); +/// ``` +#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub struct ParameterId { + segments: Vec, } -/// An [`Error`] containing multiple errors inside -pub struct ErrorsCollection(Vec); - -impl Error for ErrorsCollection {} - -/// Displays each error on a new line -impl Display for ErrorsCollection -where - T: Display, -{ +impl Display for ParameterId { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - for (i, item) in self.0.iter().enumerate() { - if i > 0 { - writeln!(f)?; + let mut print_dot = false; + for i in &self.segments { + if print_dot { + write!(f, ".")?; + } else { + print_dot = true; } - write!(f, "{item}")?; + write!(f, "{i}")?; } Ok(()) } } -impl Debug for ErrorsCollection -where - T: Debug, -{ +impl Debug for ParameterId { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - for (i, item) in self.0.iter().enumerate() { - if i > 0 { - writeln!(f)?; - } - write!(f, "{item:?}")?; - } - Ok(()) - } -} - -impl From for ErrorsCollection { - fn from(value: T) -> Self { - Self(vec![value]) + write!(f, "ParameterId({self})") } } -impl IntoIterator for ErrorsCollection { - type Item = T; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -/// An implementation of [`ReadEnv`] for testing convenience. -#[derive(Default)] -pub struct TestEnv { - map: HashMap, - visited: RefCell>, -} - -impl TestEnv { - /// Create new empty environment - pub fn new() -> Self { - Self::default() - } - - /// Create an environment with a given map - pub fn with_map(map: HashMap) -> Self { - Self { map, ..Self::new() } - } - - /// Set a key-value pair - #[must_use] - pub fn set(mut self, key: impl AsRef, value: impl AsRef) -> Self { - self.map - .insert(key.as_ref().to_string(), value.as_ref().to_string()); - self - } - - /// Get a set of keys not visited yet by [`ReadEnv::read_env`] - pub fn unvisited(&self) -> HashSet { - let all_keys: HashSet<_> = self.map.keys().map(ToOwned::to_owned).collect(); - let visited: HashSet<_> = self.visited.borrow().clone(); - all_keys.sub(&visited) - } -} - -impl ReadEnv for TestEnv { - fn read_env(&self, key: impl AsRef) -> Result>, Infallible> { - self.visited.borrow_mut().insert(key.as_ref().to_string()); - Ok(self - .map - .get(key.as_ref()) - .map(String::as_str) - .map(Cow::from)) - } -} - -/// Implemented of [`ReadEnv`] on top of [`std::env::var`]. -#[derive(Debug, Copy, Clone)] -pub struct StdEnv; - -impl ReadEnv for StdEnv { - fn read_env(&self, key: impl AsRef) -> Result>, StdEnvError> { - match std::env::var(key.as_ref()) { - Ok(value) => Ok(Some(value.into())), - Err(VarError::NotPresent) => Ok(None), - Err(VarError::NotUnicode(input)) => Err(StdEnvError::NotUnicode(input)), - } - } -} - -/// An error that might occur while reading from std env. -/// -/// - **Q: Why just [`VarError`] is not used?** -/// - A: Because [`VarError::NotPresent`] is `Ok(None)` in terms of [`ReadEnv`] -#[derive(Debug, thiserror::Error)] -pub enum StdEnvError { - /// Reflects [`VarError::NotUnicode`] - #[error("the specified environment variable was found, but it did not contain valid unicode data: {0:?}")] - NotUnicode(OsString), -} - -/// A tool that simplifies work with graceful parsing of multiple values in combination -/// with [`Emitter`] -pub enum ParseEnvResult { - /// Value was found and parsed - Value(T), - /// An error occurred while reading or parsing the environment - Error, - /// Value was not found, no error occurred - None, -} - -impl<'a> ParseEnvResult> { - fn read_env_only( - emitter: &mut Emitter, - env: &'a impl ReadEnv, - env_key: impl AsRef, - ) -> Self { - // FIXME: errors handling is such a mess now - match env - .read_env(env_key.as_ref()) - .map_err(|err| eyre!("{err}")) - .wrap_err_with(|| eyre!("ooops")) - { - Ok(Some(value)) => Self::Value(value), - Ok(None) => Self::None, - Err(report) => { - emitter.emit(report); - Self::Error - } - } - } -} - -impl ParseEnvResult -where - T: FromStr, - ::Err: Error + Send + Sync + 'static, -{ - /// _Simple_ parsing using [`FromStr`] - pub fn parse_simple( - emitter: &mut Emitter, - env: &impl ReadEnv, - env_key: impl AsRef, - field_name: impl AsRef, - ) -> Self { - let read = match ParseEnvResult::read_env_only(emitter, env, env_key.as_ref()) { - ParseEnvResult::Value(x) => x, - ParseEnvResult::None => return Self::None, - ParseEnvResult::Error => return Self::Error, - }; - - match FromStr::from_str(read.as_ref()).wrap_err_with(|| { - eyre!( - "failed to parse `{}` field from `{}` env variable", - field_name.as_ref(), - env_key.as_ref() - ) - }) { - Ok(value) => Self::Value(value), - Err(report) => { - emitter.emit(report); - Self::Error - } - } - } -} - -impl ParseEnvResult +impl

From

for ParameterId where - T: DeserializeOwned, + P: IntoIterator, +

::Item: AsRef, { - /// Treat string data in ENV as JSON - #[cfg(feature = "json")] - pub fn parse_json( - emitter: &mut Emitter, - env: &impl ReadEnv, - env_key: impl AsRef, - field_name: impl AsRef, - ) -> Self { - let read = match ParseEnvResult::read_env_only(emitter, env, env_key.as_ref()) { - ParseEnvResult::Value(x) => x, - ParseEnvResult::None => return Self::None, - ParseEnvResult::Error => return Self::Error, - }; - - match serde_json::from_str(read.as_ref()).wrap_err_with(|| { - eyre!( - "failed to parse `{}` field from `{}` env variable", - field_name.as_ref(), - env_key.as_ref() - ) - }) { - Ok(value) => Self::Value(value), - Err(report) => { - emitter.emit(report); - Self::Error - } + fn from(value: P) -> Self { + Self { + segments: value.into_iter().map(|x| x.as_ref().to_string()).collect(), } } } -/// During this conversion, [`ParseEnvResult::Error`] is interpreted as [`None`]. -impl From> for Option { - fn from(value: ParseEnvResult) -> Self { - match value { - ParseEnvResult::None | ParseEnvResult::Error => None, - ParseEnvResult::Value(x) => Some(x), - } - } +/// Indicates an origin where the value of a config parameter came from. +#[derive(Debug, Clone)] +#[allow(missing_docs)] +pub enum ParameterOrigin { + /// Value came from a file + File { id: ParameterId, path: PathBuf }, + /// Value came from an environment variable + Env { id: ParameterId, var: String }, + /// Value came from some environment variables (see [`read::ReadingParameter::env_custom`]) + EnvUnknown { id: ParameterId }, + /// It is a default value of a parameter + Default { id: ParameterId }, + /// Custom origin + Custom { message: String }, } -/// Value container to be used in the partial layers. -/// -/// In partial layers, values might be present or not. -/// Partial layers consisting from [`UserField`] might be _incomplete_, -/// merged into each other (with [`merge::Merge`]), -/// and finally unwrapped (with [`UnwrapPartial`]) into a _complete_ layer of data. -/// -/// Partial layers might consist of fields other than [`UserField`], but their types should follow -/// the same conventions. This might be used e.g. to implement custom merge strategy. -#[derive( - Serialize, - Deserialize, - Ord, - PartialOrd, - Eq, - PartialEq, - derive_more::From, - Clone, - derive_more::Deref, - derive_more::DerefMut, -)] -pub struct UserField(Option); - -/// Delegating debug repr to [`Option`] -impl Debug for UserField { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) +impl ParameterOrigin { + /// Construct [`Self::File`] + pub fn file(id: ParameterId, path: PathBuf) -> Self { + Self::File { id, path } } -} -/// Empty user field -impl Default for UserField { - fn default() -> Self { - Self(None) + /// Construct [`Self::Env`] + pub fn env(id: ParameterId, var: String) -> Self { + Self::Env { var, id } } -} -/// The other's value takes precedence over the self's -impl Merge for UserField { - fn merge(&mut self, other: Self) { - if let Some(value) = other.0 { - self.0 = Some(value) - } + /// Construct [`Self::EnvUnknown`] + pub fn env_unknown(id: ParameterId) -> Self { + Self::EnvUnknown { id } } -} - -impl UserField { - /// Get the field value - pub fn get(self) -> Option { - self.0 - } - - /// Set the field value - pub fn set(&mut self, value: T) { - self.0 = Some(value); - } -} -impl From> for UserField { - fn from(value: ParseEnvResult) -> Self { - let option: Option = value.into(); - option.into() + /// Construct [`Self::Default`] + pub fn default(id: ParameterId) -> Self { + Self::Default { id } } -} - -/// Conversion from a layer's partial state into its full state, with all required -/// fields presented. -pub trait UnwrapPartial { - /// The output of unwrapping, i.e. the full layer - type Output; - - /// Unwraps the partial into a structure with all required fields present. - /// - /// # Errors - /// If there are absent fields, returns a bulk of [`MissingFieldError`]s. - fn unwrap_partial(self) -> UnwrapPartialResult; -} -/// Used for [`UnwrapPartial::unwrap_partial`] -pub type UnwrapPartialResult = Result>; - -/// A tool to implement "extends" mechanism, i.e. mixins. -/// -/// It allows users to provide a path of other files that should be used as -/// a _base_ layer. -/// -/// ```toml -/// # contents of this file will be merged into the contents of `base.toml` -/// extends = "./base.toml" -/// ``` -/// -/// It is possible to specify multiple extensions at once: -/// -/// ```toml -/// # read `foo`, then merge `bar`, then merge `baz`, then merge this file's contents -/// extends = ["foo", "bar", "baz"] -/// ``` -/// -/// From the developer side, it should be used as a field on a partial layer: -/// -/// ``` -/// use iroha_config_base::ExtendsPaths; -/// -/// struct SomePartial { -/// extends: Option, -/// // ..other fields -/// } -/// ``` -/// -/// When this layer is constructed from a file, `ExtendsPaths` should be handled e.g. -/// with [`ExtendsPaths::iter`]. -#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] -#[serde(untagged)] -pub enum ExtendsPaths { - /// A single path to extend from - Single(PathBuf), - /// A chain of paths to extend from - Chain(Vec), -} - -/// Iterator over [`ExtendsPaths`] for convenience -pub enum ExtendsPathsIter<'a> { - #[allow(missing_docs)] - Single(Option<&'a PathBuf>), - #[allow(missing_docs)] - Multiple(std::slice::Iter<'a, PathBuf>), -} - -impl ExtendsPaths { - /// Normalise into an iterator over chain of paths to extend from - #[allow(clippy::iter_without_into_iter)] // extra for this case - pub fn iter(&self) -> ExtendsPathsIter<'_> { - match &self { - Self::Single(x) => ExtendsPathsIter::Single(Some(x)), - Self::Chain(vec) => ExtendsPathsIter::Multiple(vec.iter()), - } + /// Construct [`Self::Custom`] + pub fn custom(message: String) -> Self { + Self::Custom { message } } } -impl<'a> Iterator for ExtendsPathsIter<'a> { - type Item = &'a PathBuf; - - fn next(&mut self) -> Option { - match self { - Self::Single(x) => x.take(), - Self::Multiple(iter) => iter.next(), - } - } +/// A container with information on where the value came from, in terms of [`ParameterOrigin`] +#[derive(Debug, Clone)] +pub struct WithOrigin { + value: T, + origin: ParameterOrigin, } -#[cfg(test)] -mod tests { - use super::*; - - impl ExtendsPaths { - fn as_str_vec(&self) -> Vec<&str> { - self.iter().map(|p| p.to_str().unwrap()).collect() - } +impl WithOrigin { + /// Constructor + pub fn new(value: T, origin: ParameterOrigin) -> Self { + Self { value, origin } } - #[test] - fn single_missing_field() { - let mut emitter: Emitter = Emitter::new(); - - emitter.emit_missing_field("foo"); - - let err = emitter.finish().unwrap_err(); - - assert_eq!(format!("{err}"), "missing field: `foo`") - } - - #[test] - fn multiple_missing_fields() { - let mut emitter: Emitter = Emitter::new(); - - emitter.emit_missing_field("foo"); - emitter.emit_missing_field("bar"); - - let err = emitter.finish().unwrap_err(); - - assert_eq!( - format!("{err}"), - "missing field: `foo`\nmissing field: `bar`" + /// Construct, using caller's location as the origin. + /// + /// Primarily for testing purposes. + #[track_caller] + pub fn inline(value: T) -> Self { + Self::new( + value, + ParameterOrigin::custom(format!("inlined at `{}`", std::panic::Location::caller())), ) } - #[test] - fn merging_user_fields_overrides_old_value() { - let mut field = UserField(None); - field.merge(UserField(Some(4))); - assert_eq!(field, UserField(Some(4))); - - let mut field = UserField(Some(4)); - field.merge(UserField(Some(5))); - assert_eq!(field, UserField(Some(5))); - - let mut field = UserField(Some(4)); - field.merge(UserField(None)); - assert_eq!(field, UserField(Some(4))); + /// Borrow the value + pub fn value(&self) -> &T { + &self.value } - #[derive(Deserialize, Default)] - #[serde(default)] - struct TestExtends { - extends: Option, + /// Exclusively borrow the value + pub fn value_mut(&mut self) -> &mut T { + &mut self.value } - #[test] - fn parse_empty_extends() { - let value: TestExtends = toml::from_str("").expect("should be fine with empty input"); - - assert_eq!(value.extends, None); + /// Extract the value, dropping the origin. + /// + /// Use [`Self::into_tuple`] to extract both the value and the origin. + pub fn into_value(self) -> T { + self.value } - #[test] - fn parse_single_extends_path() { - let value: TestExtends = toml::toml! { - extends = "./path" - } - .try_into() - .unwrap(); - - assert_eq!(value.extends, Some(ExtendsPaths::Single("./path".into()))); + /// Extract the value and the origin. + /// + /// Use [`Self::into_value`] to extract only the value. + pub fn into_tuple(self) -> (T, ParameterOrigin) { + (self.value, self.origin) } - #[test] - fn parse_multiple_extends_paths() { - let value: TestExtends = toml::toml! { - extends = ["foo", "bar", "baz"] - } - .try_into() - .unwrap(); - - assert_eq!( - value.extends, - Some(ExtendsPaths::Chain(vec![ - "foo".into(), - "bar".into(), - "baz".into() - ])) - ); + /// Borrow the origin + pub fn origin(&self) -> &ParameterOrigin { + &self.origin } - #[test] - fn iterating_over_extends() { - let single = ExtendsPaths::Single("single".into()); - assert_eq!(single.as_str_vec(), vec!["single"]); - - let multi = ExtendsPaths::Chain(vec!["foo".into(), "bar".into(), "baz".into()]); - assert_eq!(multi.as_str_vec(), vec!["foo", "bar", "baz"]); + /// Construct [`ConfigValueAndOrigin`] attachment to use with [`error_stack::Report::attach_printable`]. + pub fn into_attachment(self) -> ConfigValueAndOrigin { + ConfigValueAndOrigin::new(self.value, self.origin) } - #[test] - fn deserialize_human_duration() { - #[derive(Deserialize)] - struct Test { - value: HumanDuration, + /// Convert the value with a function + pub fn map(self, fun: F) -> WithOrigin + where + F: FnOnce(T) -> U, + { + let Self { value, origin } = self; + WithOrigin { + value: fun(value), + origin, } + } +} - let Test { value } = toml::toml! { - value = 10_500 +impl> WithOrigin { + /// If the origin is [`ParameterOrigin::File`], will resolve the contained path relative to the origin. + /// Otherwise, will return the value as-is. + pub fn resolve_relative_path(&self) -> PathBuf { + match &self.origin { + ParameterOrigin::File { path, .. } => path + .parent() + .expect("if it is a file, it should have a parent path") + .join(self.value.as_ref()), + _ => self.value.as_ref().to_path_buf(), } - .try_into() - .expect("input is fine, should parse"); - - assert_eq!(value.get(), Duration::from_millis(10_500)); } } diff --git a/config/base/src/read.rs b/config/base/src/read.rs new file mode 100644 index 00000000000..2873911c635 --- /dev/null +++ b/config/base/src/read.rs @@ -0,0 +1,596 @@ +//! Configuration reader API. + +use std::{ + collections::{BTreeMap, BTreeSet}, + convert::identity, + fmt::Debug, + path::{Path, PathBuf}, +}; + +use drop_bomb::DropBomb; +use error_stack::{Context, Report, Result, ResultExt}; +use serde::Deserialize; +use thiserror::Error; + +use crate::{ + attach, + attach::EnvValue, + env::{FromEnvStr, ReadEnv}, + toml::TomlSource, + util::{Emitter, ExtendsPaths}, + ParameterId, ParameterOrigin, WithOrigin, +}; + +/// A type that implements reading from [`ConfigReader`] +pub trait ReadConfig: Sized { + /// Returns the [`FinalWrap`] with self and the reader itself, transformed + /// throughout the process of reading. + /// + /// The wrap is guaranteed to unwrap safely if the reader emits + /// no error upon [`ConfigReader::into_result`]. + fn read(reader: &mut ConfigReader) -> FinalWrap; +} + +/// An umbrella error for various cases related to [`ConfigReader`]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("Failed to read configuration from file")] + ReadFile, + #[error("Invalid `extends` field")] + InvalidExtends, + #[error("Failed to extend configurations")] + CannotExtend, + #[error("Failed to parse parameter `{0}`")] + ParseParameter(ParameterId), + #[error("Errors occurred while reading from file: `{0}`")] + InSourceFile(PathBuf), + #[error("Errors occurred while reading from environment variables")] + InEnvironment, + #[error("Some required parameters are missing")] + MissingParameters, + #[error("Found unrecognised parameters")] + UnknownParameters, + #[error("{msg}")] + Other { msg: String }, +} + +#[derive(Error, Debug)] +#[error("{0}")] +struct EnvError(String); + +impl Error { + /// Some other error message + pub fn other(message: impl AsRef) -> Self { + Self::Other { + msg: message.as_ref().to_string(), + } + } +} + +/// The reader, which provides an API to accumulate config sources, +/// read parameters from them, override with environment variables, fallback to default values, +/// and finally, construct an exhaustive error report with as many errors, accumulated along the +/// way, as possible. +pub struct ConfigReader { + /// The namespace this [`ConfigReader`] is handling. All the `ParameterId` handled will be prefixed with it. + nesting: Vec, + /// File sources for the config + sources: Vec, + /// Environment variables source for the config + env: Box, + /// Errors accumulated per each file + errors_by_source: BTreeMap>>, + /// Errors accumulated from the environment variables + errors_in_env: Vec>, + /// A list of all the parameters that have been requested from this reader. Used to report unused (unknown) parameters in the toml file + existing_parameters: BTreeSet, + /// A list of all required parameters that have been requested, but were not found + missing_parameters: BTreeSet, + /// A runtime guard to prevent dropping the [`ConfigReader`] without handing errors + bomb: DropBomb, +} + +impl Debug for ConfigReader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ConfigReader") + } +} + +impl Default for ConfigReader { + fn default() -> Self { + Self::new() + } +} + +impl ConfigReader { + /// Constructor + pub fn new() -> Self { + Self { + sources: <_>::default(), + nesting: <_>::default(), + errors_by_source: <_>::default(), + errors_in_env: <_>::default(), + existing_parameters: <_>::default(), + missing_parameters: <_>::default(), + bomb: DropBomb::new("forgot to call `ConfigReader::finish()`, didn't you?"), + env: Box::new(crate::env::std_env), + } + } + + /// Replace default environment reader ([`std::env::var`]) with a custom one + #[must_use] + pub fn with_env(mut self, env: impl ReadEnv + 'static) -> Self { + self.env = Box::new(env); + self + } + + /// Add a data source to read parameters from. + #[must_use] + pub fn with_toml_source(mut self, source: TomlSource) -> Self { + self.sources.push(source); + self + } + + /// Reads a TOML file and handles its `extends` field, implementing mixins mechanism. + /// + /// # Errors + /// + /// If files reading error occurs + pub fn read_toml_with_extends>(mut self, path: P) -> Result { + fn recursion( + reader: &mut ConfigReader, + path: impl AsRef, + depth: u8, + ) -> Result<(), Error> { + let mut source = TomlSource::from_file(path.as_ref()) + .attach_printable_lazy(|| attach::FilePath::new(path.as_ref().to_path_buf())) + .change_context(Error::ReadFile)?; + let table = source.table_mut(); + + if let Some(extends) = table.remove("extends") { + let parsed: ExtendsPaths = extends.clone() + .try_into() + .attach_printable_lazy(|| attach::Expected::new(r#"a single path ("./file.toml") or an array of paths (["a.toml", "b.toml", "c.toml"])"#)) + .attach_printable_lazy(|| attach::ActualValue::new(extends)) + .change_context(Error::InvalidExtends)?; + log::trace!("found `extends`: {:?}", parsed); + for extends_path in parsed.iter() { + let full_path = path + .as_ref() + .parent() + .expect("it cannot be root or empty") + .join(extends_path); + + recursion(reader, &full_path, depth + 1).attach_printable_lazy(|| { + attach::ExtendsChain::new(path.as_ref().to_path_buf(), full_path, depth + 1) + })?; + } + }; + + reader.sources.push(source); + + Ok(()) + } + + recursion(&mut self, path.as_ref(), 0).map_err(|err| { + // error doesn't mean we need to panic + self.bomb.defuse(); + err + })?; + + Ok(self) + } + + /// Instantiate a parameter reading pipeline. + #[must_use] + pub fn read_parameter(&mut self, id: impl Into) -> ReadingParameter + where + for<'de> T: Deserialize<'de>, + { + let id = self.full_id(id); + self.collect_parameter(&id); + ReadingParameter::new(self, id).fetch() + } + + /// Delegate reading to another implementor of [`ReadConfig`] under a certain namespace. + /// All parameter IDs in it will be resolved within that namespace. + #[must_use] + pub fn read_nested(&mut self, namespace: impl AsRef) -> FinalWrap { + self.nesting.push(namespace.as_ref().to_string()); + let value = T::read(self); + self.nesting.pop(); + value + } + + /// Finally, complete the reading procedure and emit a collective report + /// in case if any error occurred along the reading process. + /// + /// # Errors + /// If any occurred while reading of data. + pub fn into_result(mut self) -> Result<(), Error> { + self.bomb.defuse(); + let mut emitter = Emitter::new(); + + if !self.missing_parameters.is_empty() { + let mut report = Report::new(Error::MissingParameters); + for i in self.missing_parameters { + report = report.attach_printable(format!("missing parameter: `{i}`")); + } + emitter.emit(report); + } + + // looking for unknown parameters + for source in &self.sources { + let unknown_parameters = source.find_unknown(self.existing_parameters.iter()); + if !unknown_parameters.is_empty() { + let mut report = Report::new(Error::UnknownParameters); + for i in unknown_parameters { + report = report.attach_printable(format!("unknown parameter: `{i}`")); + } + self.errors_by_source + .entry(source.path().clone()) + .or_default() + .push(report); + } + } + + // emit reports by source + for (source, reports) in self.errors_by_source { + let mut local_emitter = Emitter::new(); + for report in reports { + local_emitter.emit(report); + } + let report = local_emitter + .into_result() + .expect_err("there should be at least one error"); + emitter.emit(report.change_context(Error::InSourceFile(source))) + } + + // environment parsing errors + if !self.errors_in_env.is_empty() { + let mut local_emitter = Emitter::new(); + for report in self.errors_in_env { + local_emitter.emit(report); + } + let report = local_emitter + .into_result() + .expect_err("there should be at least one error"); + emitter.emit(report.change_context(Error::InEnvironment)); + } + + emitter.into_result() + } + + /// A shorthand to "just read the config and get an error or the value". + /// # Errors + /// See [`Self::into_result`] + pub fn read_and_complete(mut self) -> Result { + let value = T::read(&mut self); + self.into_result()?; + Ok(value.unwrap()) + } + + fn full_id(&self, id: impl Into) -> ParameterId { + self.nesting.iter().chain(id.into().segments.iter()).into() + } + + fn collect_deserialize_error( + &mut self, + source: &TomlSource, + path: &ParameterId, + report: Report, + ) { + self.errors_by_source + .entry(source.path().clone()) + .or_default() + .push(report.change_context(Error::ParseParameter(path.clone()))); + } + + fn collect_env_error(&mut self, report: Report) { + self.errors_in_env.push(report) + } + + fn collect_parameter(&mut self, id: &ParameterId) { + self.existing_parameters.insert(id.clone()); + } + + fn collect_missing_parameter(&mut self, id: &ParameterId) { + self.missing_parameters.insert(id.clone()); + } + + fn fetch_parameter( + &mut self, + id: &ParameterId, + ) -> core::result::Result>, ()> + where + for<'de> T: Deserialize<'de>, + { + self.collect_parameter(id); + + let mut errored = false; + let mut value = None; + let mut errors = Vec::default(); + + for source in &self.sources { + if let Some(toml_value) = source.fetch(id) { + // FIXME: Avoid cloning. + // Currently cloning is performed for the sake of a better error message. + // In future, it might be replaced with a rendered source TOML and span to it + let result: core::result::Result = toml_value.clone().try_into(); + match (result, errored) { + (Ok(v), false) => { + if value.is_none() { + log::trace!("parameter `{id}`: found in `{}`", source.path().display()); + } else { + log::trace!( + "parameter `{id}`: found in `{}`, overwriting previous value", + source.path().display() + ); + } + value = Some(WithOrigin::new( + v, + ParameterOrigin::file(id.clone(), source.path().clone()), + )); + } + // we don't care if there was an error before + (Ok(_), true) => {} + (Err(error), _) => { + errored = true; + value = None; + errors.push(( + Report::new(error).attach_printable(format!("value: {toml_value}")), + source.clone(), + )); + } + } + } else { + log::trace!( + "parameter `{id}`: not found in `{}`", + source.path().display() + ) + } + } + + for (error, source) in errors { + self.collect_deserialize_error(&source, id, error); + } + + if errored { + Err(()) + } else { + Ok(value) + } + } +} + +/// A state of reading a certain configuration parameter. +pub struct ReadingParameter<'reader, T> { + reader: &'reader mut ConfigReader, + id: ParameterId, + value: Option>, + errored: bool, +} + +impl<'reader, T> ReadingParameter<'reader, T> { + fn new(reader: &'reader mut ConfigReader, id: ParameterId) -> Self { + Self { + reader, + id, + value: None, + errored: false, + } + } +} + +impl ReadingParameter<'_, T> +where + for<'de> T: Deserialize<'de>, +{ + #[must_use] + fn fetch(mut self) -> Self { + match self.reader.fetch_parameter(&self.id) { + Ok(value) => { + self.value = value; + } + Err(()) => { + self.errored = true; + } + } + + self + } +} + +impl ReadingParameter<'_, T> +where + T: FromEnvStr, +{ + /// Reads an environment variable and parses the value which is [`FromEnvStr`]. + #[must_use] + pub fn env(mut self, var: impl AsRef) -> Self { + let var = var.as_ref(); + if let Some(raw_str) = self.reader.env.read_env(var) { + match (T::from_env_str(raw_str.clone()), self.errored) { + (Err(error), _) => { + self.errored = true; + self.reader.collect_env_error( + Report::new(error) + .attach_printable(EnvValue::new(var.to_string(), raw_str.into_owned())) + .change_context(EnvError(format!( + "Failed to parse parameter `{}` from `{var}`", + self.id, + ))), + ); + } + (Ok(value), false) => { + if self.value.is_none() { + log::trace!("parameter `{}`: found `{var}` env var", self.id,); + } else { + log::trace!( + "parameter `{}`: found `{var}` env var, overwriting previous value", + self.id, + ); + } + self.value = Some(WithOrigin::new( + value, + ParameterOrigin::env(self.id.clone(), var.to_string()), + )); + } + (Ok(_ignore), true) => { + log::trace!( + "parameter `{}`: env var `{var}` found, ignore due to previous errors", + self.id, + ); + } + } + } else { + log::trace!("parameter `{}`: env var `{var}` not found", self.id) + } + + self + } +} + +impl ReadingParameter<'_, T> { + /// Finish reading, and if the value is not read so far, it will be reported later on [`ConfigReader::into_result`]. + #[must_use] + pub fn value_required(self) -> ReadingDone { + match (self.errored, self.value) { + (false, Some(value)) => ReadingDone(ReadingDoneValue::Fine(value)), + (false, None) => { + self.reader.collect_missing_parameter(&self.id); + ReadingDone(ReadingDoneValue::Errored) + } + (true, _) => ReadingDone(ReadingDoneValue::Errored), + } + } + + /// Finish reading, falling back to a default value if it is absent + #[must_use] + pub fn value_or_else T>(self, fun: F) -> ReadingDone { + match (self.errored, self.value) { + (false, Some(value)) => ReadingDone(ReadingDoneValue::Fine(value)), + (false, None) => { + log::trace!("parameter `{}`: fallback to default value", self.id); + ReadingDone(ReadingDoneValue::Fine(WithOrigin::new( + fun(), + ParameterOrigin::default(self.id.clone()), + ))) + } + (true, _) => ReadingDone(ReadingDoneValue::Errored), + } + } + + /// Finish reading, allowing value to be not present + #[must_use] + pub fn value_optional(self) -> OptionReadingDone { + match (self.errored, self.value) { + (false, value) => OptionReadingDone(ReadingDoneValue::Fine(value)), + (true, _) => OptionReadingDone(ReadingDoneValue::Errored), + } + } +} + +// TODO check lifetime redundancy +impl ReadingParameter<'_, T> { + /// Equivalent of [`ReadingParameter::value_or_else`] with [`Default::default`]. + #[must_use] + pub fn value_or_default(self) -> ReadingDone { + self.value_or_else(Default::default) + } +} + +enum ReadingDoneValue { + Errored, + Fine(T), +} + +impl ReadingDoneValue { + fn into_final(self) -> FinalWrap { + self.into_final_with(identity) + } + + fn into_final_with(self, f: F) -> FinalWrap + where + F: FnOnce(T) -> U, + { + match self { + Self::Errored => FinalWrap(FinalWrapInner::Errored), + Self::Fine(t) => FinalWrap(FinalWrapInner::Value(f(t))), + } + } +} + +/// A state of reading when the parameter's value is read, and the next step is to finish it via +/// [`ReadingDone::finish`] or [`ReadingDone::finish_with_origin`] +pub struct ReadingDone(ReadingDoneValue>); + +/// Same as [`ReadingDone`], but holding an optional value. +pub struct OptionReadingDone(ReadingDoneValue>>); + +impl ReadingDone { + /// Finish with the value only. + #[must_use] + pub fn finish(self) -> FinalWrap { + self.0.into_final_with(WithOrigin::into_value) + } + + /// Finish with the value and its origin + #[must_use] + pub fn finish_with_origin(self) -> FinalWrap> { + self.0.into_final() + } +} + +impl OptionReadingDone { + /// Finish with the value only + #[must_use] + pub fn finish(self) -> FinalWrap> { + self.0.into_final_with(|x| x.map(WithOrigin::into_value)) + } + + /// Finish with the value and its origin + #[must_use] + pub fn finish_with_origin(self) -> FinalWrap>> { + self.0.into_final() + } +} + +/// A value that should be accessed only if overall configuration reading succeeded. +/// +/// I.e. it is guaranteed that [`FinalWrap::unwrap`] will not panic after associated +/// [`ConfigReader::into_result`] returns [`Ok`]. +#[allow(missing_docs)] +pub struct FinalWrap(FinalWrapInner); + +/// Exists to not expose enum variants if they were in [`FinalWrap`] +enum FinalWrapInner { + Errored, + Value(T), + ValueFn(Box T>), +} + +impl FinalWrap { + /// Pass a closure that will emit the value on [`Self::unwrap`]. + pub fn value_fn(fun: F) -> Self + where + F: FnOnce() -> T + 'static, + { + Self(FinalWrapInner::ValueFn(Box::new(fun))) + } + + /// Unwrap the value inside. + /// + /// Can be safely called only after the [`ConfigReader::into_result`] returned [Ok]. + /// + /// # Panics + /// Might panic if an error occurred while reading of this certain value. + pub fn unwrap(self) -> T { + match self.0 { + FinalWrapInner::Errored => panic!("`FinalWrap::unwrap` is supposed to be called only after `ConfigReader::into_result` returns OK; it is probably a bug"), + FinalWrapInner::Value(value) => value, + FinalWrapInner::ValueFn(fun) => fun() + } + } +} diff --git a/config/base/src/toml.rs b/config/base/src/toml.rs new file mode 100644 index 00000000000..3ca9e4ce5ba --- /dev/null +++ b/config/base/src/toml.rs @@ -0,0 +1,428 @@ +//! TOML-specific tools. +//! +//! While it is definitely possible to support other formats than TOML, since there is no +//! need for this for now, TOML support is integrated in a non-generic way. + +use std::{ + collections::{BTreeMap, BTreeSet}, + fs::File, + io::Read, + path::{Path, PathBuf}, + str::FromStr, +}; + +use error_stack::ResultExt; +use serde::Serialize; +use thiserror::Error; +use toml::Table; + +use crate::ParameterId; + +/// A source of configuration in TOML format +#[derive(Debug, Clone)] +pub struct TomlSource { + path: PathBuf, + table: Table, +} + +/// Error of [`TomlSource::from_file`] +#[derive(Error, Debug, Copy, Clone)] +#[allow(missing_docs)] +pub enum FromFileError { + #[error("File system error")] + Read, + #[error("Error while deserializing file contents as TOML")] + Parse, +} + +impl TomlSource { + /// Constructor + pub fn new(path: PathBuf, table: Table) -> Self { + Self { path, table } + } + + /// Read from a file + /// + /// # Errors + /// If a file system or a TOML parsing error occurs. + pub fn from_file>(path: P) -> error_stack::Result { + let path = path.as_ref().to_path_buf(); + + log::trace!("reading TOML source: `{}`", path.display()); + + let mut raw_string = String::new(); + File::open(&path) + .change_context(FromFileError::Read)? + .read_to_string(&mut raw_string) + .change_context(FromFileError::Read)?; + + let table = Table::from_str(&raw_string).change_context(FromFileError::Parse)?; + + Ok(TomlSource::new(path, table)) + } + + /// Primarily for testing purposes: creates a source which will contain debug information + /// about where this source was defined. + #[track_caller] + pub fn inline(table: Table) -> Self { + Self::new( + PathBuf::from(format!("inline:{}", std::panic::Location::caller())), + table, + ) + } + + /// Get an exclusive borrow of the TOML table inside + pub fn table_mut(&mut self) -> &mut Table { + &mut self.table + } + + /// Fetch a value by parameter path + // FIXME: not optimal code + // TODO: implement via `Index` trait? + pub fn fetch(&self, path: &ParameterId) -> Option { + enum TableOrValue<'a> { + Table(&'a Table), + Value(&'a toml::Value), + } + + let mut value = TableOrValue::Table(&self.table); + + for segment in &path.segments { + let table = match value { + TableOrValue::Table(table) | TableOrValue::Value(toml::Value::Table(table)) => { + table + } + _ => return None, + }; + value = TableOrValue::Value(table.get(segment)?); + } + + // FIXME: cloning + match value { + TableOrValue::Table(table) => Some(toml::Value::Table(table.clone())), + TableOrValue::Value(value) => Some(value.clone()), + } + } + + /// Get the file path of the source + pub fn path(&self) -> &PathBuf { + &self.path + } + + // FIXME: false-positive + // https://github.com/rust-lang/rust/issues/44752#issuecomment-1712086069 + #[allow(single_use_lifetimes)] + pub(crate) fn find_unknown<'a>( + &self, + known: impl Iterator, + ) -> BTreeSet { + find_unknown_parameters(&self.table, &known.into()) + } +} + +#[derive(Default)] +struct ParamTree<'a>(BTreeMap<&'a str, ParamTree<'a>>); + +impl std::fmt::Debug for ParamTree<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl<'a, T> From for ParamTree<'a> +where + T: Iterator, +{ + fn from(value: T) -> Self { + let mut tree = Self(<_>::default()); + for path in value { + let mut tree_tmp = &mut tree; + for segment in &path.segments { + tree_tmp = tree_tmp.0.entry(segment).or_default(); + } + } + tree + } +} + +fn find_unknown_parameters(table: &toml::Table, known: &ParamTree) -> BTreeSet { + #[derive(Default)] + struct Traverse<'a> { + current_path: Vec<&'a str>, + unknown: BTreeSet, + } + + impl<'a> Traverse<'a> { + fn run(mut self, table: &'a toml::Table, known: &ParamTree) -> Self { + for (key, value) in table { + if let Some(known) = known.0.get(key.as_str()) { + // we are in the "known" + if known.0.is_empty() { + // we reached the boundary of explicit "known". + // everything below is implied to be known + } else if let toml::Value::Table(nested) = value { + self.current_path.push(key.as_str()); + self = self.run(nested, known); + self.current_path.pop(); + } + } else { + // we are in the "unknown" + let unknown_path = self + .current_path + .iter() + .chain(std::iter::once(&key.as_str())) + .into(); + self.unknown.insert(unknown_path); + } + } + + self + } + } + + Traverse::default().run(table, known).unknown +} + +/// A utility, primarily for testing, to conveniently write content into a [`Table`]. +/// +/// ``` +/// use iroha_config_base::toml::Writer; +/// use toml::Table; +/// +/// let mut table = Table::new(); +/// Writer::new(&mut table) +/// .write("foo", "some string") +/// .write("bar", "some other string") +/// .write(["baz", "foo", "bar"], 42); +/// +/// assert_eq!( +/// table, +/// toml::toml! { +/// foo = "some string" +/// bar = "some other string" +/// +/// [baz.foo] +/// bar = 42 +/// } +/// ); +/// ``` +#[derive(Debug)] +pub struct Writer<'a> { + table: &'a mut Table, +} + +impl<'a> Writer<'a> { + /// Constructor + pub fn new(table: &'a mut Table) -> Self { + Self { table } + } + + /// Write a serializable value by path. + /// Recursively creates all path segments as tables if they don't exist. + /// + /// # Panics + /// + /// - If there is existing non-table value along the path + /// - If value cannot serialize into [`toml::Value`] + pub fn write(&'a mut self, path: P, value: T) -> &'a mut Self { + let mut current: Option<(&mut Table, &str)> = None; + + for i in path.path() { + if let Some((table, key)) = current { + let table = table + .entry(key) + .or_insert(toml::Value::Table(<_>::default())) + .as_table_mut() + .expect("expected a table"); + current = Some((table, i)) + } else { + // IDK why Rust allows it + current = Some((self.table, i)) + } + } + + if let Some((table, key)) = current { + let value_toml = toml::Value::try_from(value).expect("value should be a valid TOML"); + table.insert(key.to_string(), value_toml); + } + + self + } +} + +/// Allows polymorphism for a field path in [`Writer::write`]: +/// +/// ``` +/// use iroha_config_base::toml::Writer; +/// +/// let mut table = toml::Table::new(); +/// Writer::new(&mut table) +/// // path: .fine +/// .write("fine", 0) +/// // path: .also.fine +/// .write(["also", "fine"], 1); +/// ``` +pub trait WritePath { + /// Provides an iterator over path segments + fn path(self) -> impl IntoIterator; +} + +impl WritePath for &'static str { + fn path(self) -> impl IntoIterator { + [self] + } +} + +impl WritePath for [&'static str; N] { + fn path(self) -> impl IntoIterator { + self + } +} + +impl<'a> From<&'a mut Table> for Writer<'a> { + fn from(value: &'a mut Table) -> Self { + Self::new(value) + } +} + +#[cfg(test)] +mod tests { + use expect_test::expect; + use toml::toml; + + use super::*; + + #[test] + fn create_param_tree() { + let params = [ + ParameterId::from(["a", "b", "c"]), + ParameterId::from(["a", "b", "d"]), + ParameterId::from(["b", "a", "c"]), + ParameterId::from(["foo", "bar"]), + ]; + + let map = ParamTree::from(params.iter()); + + expect![[r#" + { + "a": { + "b": { + "c": {}, + "d": {}, + }, + }, + "b": { + "a": { + "c": {}, + }, + }, + "foo": { + "bar": {}, + }, + }"#]] + .assert_eq(&format!("{map:#?}")); + } + + #[test] + fn unknown_params_in_empty_are_empty() { + let known = [ + ParameterId::from(["foo", "bar"]), + ParameterId::from(["foo", "baz"]), + ]; + let known: ParamTree = known.iter().into(); + let table = toml::Table::new(); + + let unknown = find_unknown_parameters(&table, &known); + + assert_eq!(unknown, <_>::default()); + } + + #[test] + fn with_empty_known_finds_root_unknowns() { + let table = toml! { + [foo] + bar = "hey" + + [baz] + foo = 412 + }; + + let unknown = find_unknown_parameters(&table, &<_>::default()); + + let expected = [ParameterId::from(["foo"]), ParameterId::from(["baz"])] + .into_iter() + .collect(); + assert_eq!(unknown, expected); + } + + #[test] + fn unknown_depth_2() { + let known = [ + ParameterId::from(["foo", "bar"]), + ParameterId::from(["foo", "baz"]), + ]; + let known = ParamTree::from(known.iter()); + let table = toml! { + [foo] + bar = 42 + baz = "known" + foo.bar = { unknown = true } + }; + + let unknown = find_unknown_parameters(&table, &known); + + let expected = vec![ParameterId::from(["foo", "foo"])] + .into_iter() + .collect(); + assert_eq!(unknown, expected); + } + + #[test] + fn nested_into_known_are_ok() { + let known = [ParameterId::from(["a"])]; + let known = ParamTree::from(known.iter()); + let table = toml! { + [a] + b = 4 + c = 12 + }; + + let unknown = find_unknown_parameters(&table, &known); + + assert_eq!(unknown, <_>::default()); + } + + #[test] + fn writing_into_toml_works() { + #[derive(Serialize)] + struct Complex { + foo: bool, + bar: bool, + } + + let mut table = Table::new(); + + Writer::new(&mut table) + .write("foo", "test") + .write(["bar", "foo"], 42) + .write( + ["bar", "complex"], + &Complex { + foo: false, + bar: true, + }, + ); + + expect![[r#" + foo = "test" + + [bar] + foo = 42 + + [bar.complex] + bar = true + foo = false + "#]] + .assert_eq(&toml::to_string_pretty(&table).unwrap()); + } +} diff --git a/config/base/src/util.rs b/config/base/src/util.rs new file mode 100644 index 00000000000..99fa86131fa --- /dev/null +++ b/config/base/src/util.rs @@ -0,0 +1,255 @@ +//! Various utilities + +use std::{path::PathBuf, time::Duration}; + +use derive_more::Display; +use drop_bomb::DropBomb; +use error_stack::Report; +use serde::{Deserialize, Serialize}; + +/// [`Duration`], but can parse a human-readable string. +/// TODO: currently deserializes just as [`Duration`] +#[serde_with::serde_as] +#[derive(Debug, Copy, Clone, Deserialize, Serialize, Ord, PartialOrd, Eq, PartialEq, Display)] +#[display(fmt = "{_0:?}")] +pub struct HumanDuration(#[serde_as(as = "serde_with::DurationMilliSeconds")] pub Duration); + +impl HumanDuration { + /// Get the [`Duration`] + pub fn get(self) -> Duration { + self.0 + } +} + +impl From for HumanDuration { + fn from(value: Duration) -> Self { + Self(value) + } +} + +/// Representation of number of bytes, parseable from a human-readable string. +#[derive(Debug, Copy, Clone, Deserialize, Serialize)] +pub struct HumanBytes(pub T); + +impl HumanBytes { + /// Get the number of bytes + pub fn get(self) -> T { + self.0 + } +} + +impl From for HumanBytes { + fn from(value: T) -> Self { + Self(value) + } +} + +/// A tool to implement "extends" mechanism, i.e. mixins. +/// +/// It allows users to provide a path of other files that should be used as +/// a _base_ layer. +/// +/// ```toml +/// # contents of this file will be merged into the contents of `base.toml` +/// extends = "./base.toml" +/// ``` +/// +/// It is possible to specify multiple extensions at once: +/// +/// ```toml +/// # read `foo`, then merge `bar`, then merge `baz`, then merge this file's contents +/// extends = ["foo", "bar", "baz"] +/// ``` +/// +/// From the developer side, it should be used as a field on a partial layer: +/// +/// ``` +/// use iroha_config_base::util::ExtendsPaths; +/// +/// struct SomePartial { +/// extends: Option, +/// // ..other fields +/// } +/// ``` +/// +/// When this layer is constructed from a file, `ExtendsPaths` should be handled e.g. +/// with [`ExtendsPaths::iter`]. +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(untagged)] +pub enum ExtendsPaths { + /// A single path to extend from + Single(PathBuf), + /// A chain of paths to extend from + Chain(Vec), +} + +/// Iterator over [`ExtendsPaths`] for convenience +pub enum ExtendsPathsIter<'a> { + #[allow(missing_docs)] + Single(Option<&'a PathBuf>), + #[allow(missing_docs)] + Chain(std::slice::Iter<'a, PathBuf>), +} + +impl ExtendsPaths { + /// Normalize into an iterator over a chain of paths to extend from + #[allow(clippy::iter_without_into_iter)] // extra for this case + pub fn iter(&self) -> ExtendsPathsIter<'_> { + match &self { + Self::Single(x) => ExtendsPathsIter::Single(Some(x)), + Self::Chain(vec) => ExtendsPathsIter::Chain(vec.iter()), + } + } +} + +impl<'a> Iterator for ExtendsPathsIter<'a> { + type Item = &'a PathBuf; + + fn next(&mut self) -> Option { + match self { + Self::Single(x) => x.take(), + Self::Chain(iter) => iter.next(), + } + } +} + +/// A tool to collect multiple [`Report`]s. +/// +/// Will panic on [`Drop`] unless [`Emitter::into_result`] is called. +#[derive(Debug)] +pub struct Emitter { + report: Option>, + bomb: DropBomb, +} + +impl Default for Emitter { + fn default() -> Self { + Self::new() + } +} + +impl Emitter { + /// Constructor + pub fn new() -> Self { + Self { + report: None, + bomb: DropBomb::new("haven't called `Emitter::into_result()`, have you?"), + } + } + + /// Emit a single report + pub fn emit(&mut self, report: Report) { + match &mut self.report { + Some(existing) => { + existing.extend_one(report); + } + None => { + self.report = Some(report); + } + } + } + + /// Convert into [`Err`] if any report was emitted, otherwise [`Ok`]. + /// # Errors + /// If at least one report was emitted. + pub fn into_result(mut self) -> error_stack::Result<(), C> { + self.bomb.defuse(); + self.report.map_or_else(|| Ok(()), Err) + } +} + +/// An extension of [Result] to add convenience methods to work with [Emitter]. +pub trait EmitterResultExt { + /// If [Ok], return [Some]; otherwise, emit an error and return [None]. + fn ok_or_emit(self, emitter: &mut Emitter) -> Option; +} + +impl EmitterResultExt for error_stack::Result { + fn ok_or_emit(self, emitter: &mut Emitter) -> Option { + self.map_or_else( + |report| { + emitter.emit(report); + None + }, + Some, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Deserialize, Default)] + #[serde(default)] + struct TestExtends { + extends: Option, + } + + #[test] + fn parse_empty_extends() { + let value: TestExtends = toml::from_str("").expect("should be fine with empty input"); + + assert_eq!(value.extends, None); + } + + #[test] + fn parse_single_extends_path() { + let value: TestExtends = toml::toml! { + extends = "./path" + } + .try_into() + .unwrap(); + + assert_eq!(value.extends, Some(ExtendsPaths::Single("./path".into()))); + } + + #[test] + fn parse_multiple_extends_paths() { + let value: TestExtends = toml::toml! { + extends = ["foo", "bar", "baz"] + } + .try_into() + .unwrap(); + + assert_eq!( + value.extends, + Some(ExtendsPaths::Chain(vec![ + "foo".into(), + "bar".into(), + "baz".into() + ])) + ); + } + + #[test] + fn iterating_over_extends() { + impl ExtendsPaths { + fn as_str_vec(&self) -> Vec<&str> { + self.iter().map(|p| p.to_str().unwrap()).collect() + } + } + + let single = ExtendsPaths::Single("single".into()); + assert_eq!(single.as_str_vec(), vec!["single"]); + + let multi = ExtendsPaths::Chain(vec!["foo".into(), "bar".into(), "baz".into()]); + assert_eq!(multi.as_str_vec(), vec!["foo", "bar", "baz"]); + } + + #[test] + fn deserialize_human_duration() { + #[derive(Deserialize)] + struct Test { + value: HumanDuration, + } + + let Test { value } = toml::toml! { + value = 10_500 + } + .try_into() + .expect("input is fine, should parse"); + + assert_eq!(value.get(), Duration::from_millis(10_500)); + } +} diff --git a/config/base/tests/bad.invalid-extends.toml b/config/base/tests/bad.invalid-extends.toml new file mode 100644 index 00000000000..a1783010ed1 --- /dev/null +++ b/config/base/tests/bad.invalid-extends.toml @@ -0,0 +1 @@ +extends = 1234 \ No newline at end of file diff --git a/config/base/tests/bad.invalid-nested-extends.base.toml b/config/base/tests/bad.invalid-nested-extends.base.toml new file mode 100644 index 00000000000..29e469e87de --- /dev/null +++ b/config/base/tests/bad.invalid-nested-extends.base.toml @@ -0,0 +1 @@ +extends = ["non-existing.toml"] \ No newline at end of file diff --git a/config/base/tests/bad.invalid-nested-extends.toml b/config/base/tests/bad.invalid-nested-extends.toml new file mode 100644 index 00000000000..24cfdb4882f --- /dev/null +++ b/config/base/tests/bad.invalid-nested-extends.toml @@ -0,0 +1 @@ +extends = ["bad.invalid-nested-extends.base.toml"] diff --git a/config/base/tests/misc.rs b/config/base/tests/misc.rs new file mode 100644 index 00000000000..8ea64ba8493 --- /dev/null +++ b/config/base/tests/misc.rs @@ -0,0 +1,488 @@ +#![allow(clippy::needless_raw_string_hashes)] + +use std::{backtrace::Backtrace, panic::Location, path::PathBuf}; + +use error_stack::{fmt::ColorMode, Context, Report}; +use expect_test::expect; +use iroha_config_base::{env::MockEnv, read::ConfigReader, toml::TomlSource}; +use toml::toml; + +pub mod sample_config { + use std::{net::SocketAddr, path::PathBuf}; + + use iroha_config_base::{ + read::{ConfigReader, FinalWrap, ReadConfig}, + WithOrigin, + }; + use serde::Deserialize; + + #[derive(Debug)] + pub struct Root { + pub chain_id: String, + pub torii: Torii, + pub kura: Kura, + pub telemetry: Telemetry, + pub logger: Logger, + } + + impl ReadConfig for Root { + fn read(reader: &mut ConfigReader) -> FinalWrap + where + Self: Sized, + { + let chain_id = reader + .read_parameter::(["chain_id"]) + .env("CHAIN_ID") + .value_required() + .finish(); + + let torii = reader.read_nested("torii"); + + let kura = reader.read_nested("kura"); + + let telemetry = reader.read_nested("telemetry"); + + let logger = reader.read_nested("logger"); + + FinalWrap::value_fn(move || Self { + chain_id: chain_id.unwrap(), + torii: torii.unwrap(), + kura: kura.unwrap(), + telemetry: telemetry.unwrap(), + logger: logger.unwrap(), + }) + } + } + + #[derive(Debug)] + pub struct Torii { + pub address: WithOrigin, + pub max_content_len: u64, + } + + impl ReadConfig for Torii { + fn read(reader: &mut ConfigReader) -> FinalWrap + where + Self: Sized, + { + let address = reader + .read_parameter::(["address"]) + .env("API_ADDRESS") + .value_or_else(|| "128.0.0.1:8080".parse().unwrap()) + .finish_with_origin(); + + let max_content_len = reader + .read_parameter::(["max_content_length"]) + .value_or_else(|| 1024) + .finish(); + + FinalWrap::value_fn(|| Self { + address: address.unwrap(), + max_content_len: max_content_len.unwrap(), + }) + } + } + + #[derive(Debug)] + pub struct Kura { + pub store_dir: WithOrigin, + pub debug_force: bool, + } + + impl ReadConfig for Kura { + fn read(reader: &mut ConfigReader) -> FinalWrap + where + Self: Sized, + { + // origin needed so that we can resolve the path relative to the origin + let store_dir = reader + .read_parameter::(["store_dir"]) + .env("KURA_STORE_DIR") + .value_or_else(|| PathBuf::from("./storage")) + .finish_with_origin(); + + let debug_force = reader + .read_parameter::(["debug_force"]) + .value_or_else(|| false) + .finish(); + + FinalWrap::value_fn(|| Self { + store_dir: store_dir.unwrap(), + debug_force: debug_force.unwrap(), + }) + } + } + + #[derive(Debug)] + pub struct Telemetry { + pub out_file: Option>, + } + + impl ReadConfig for Telemetry { + fn read(reader: &mut ConfigReader) -> FinalWrap + where + Self: Sized, + { + // origin needed so that we can resolve the path relative to the origin + let out_file = reader + .read_parameter::(["dev", "out_file"]) + .value_optional() + .finish_with_origin(); + + FinalWrap::value_fn(|| Self { + out_file: out_file.unwrap(), + }) + } + } + + #[derive(Debug, Copy, Clone)] + pub struct Logger { + pub level: LogLevel, + } + + impl ReadConfig for Logger { + fn read(reader: &mut ConfigReader) -> FinalWrap + where + Self: Sized, + { + let level = reader + .read_parameter::(["level"]) + .env("LOG_LEVEL") + .value_or_default() + .finish(); + + FinalWrap::value_fn(|| Self { + level: level.unwrap(), + }) + } + } + + #[derive(Deserialize, Debug, Default, strum::Display, strum::EnumString, Copy, Clone)] + pub enum LogLevel { + Debug, + #[default] + Info, + Warning, + Error, + } +} + +fn format_report(report: &Report) -> String { + Report::install_debug_hook::(|_value, _context| { + // noop + }); + + Report::install_debug_hook::(|_value, _context| { + // noop + }); + + Report::set_color_mode(ColorMode::None); + + format!("{report:#?}") +} + +trait ExpectExt { + fn assert_eq_report(&self, report: &Report); +} + +impl ExpectExt for expect_test::Expect { + fn assert_eq_report(&self, report: &Report) { + self.assert_eq(&format_report(report)); + } +} + +#[test] +fn error_when_no_file() { + let report = ConfigReader::new() + .read_toml_with_extends("/path/to/non/existing...") + .expect_err("the path doesn't exist"); + + expect![[r#" + Failed to read configuration from file + │ + ├─▶ File system error + │ ╰╴file path: /path/to/non/existing... + │ + ╰─▶ No such file or directory (os error 2)"#]] + .assert_eq_report(&report); +} + +#[test] +fn error_invalid_extends() { + let report = ConfigReader::new() + .read_toml_with_extends("./tests/bad.invalid-extends.toml") + .expect_err("extends is invalid, should fail"); + + expect![[r#" + Invalid `extends` field + │ + ╰─▶ data did not match any variant of untagged enum ExtendsPaths + ├╴expected: a single path ("./file.toml") or an array of paths (["a.toml", "b.toml", "c.toml"]) + ╰╴actual value: 1234"#]] + .assert_eq_report(&report); +} + +#[test] +fn error_extends_depth_2_leads_to_nowhere() { + let report = ConfigReader::new() + .read_toml_with_extends("./tests/bad.invalid-nested-extends.toml") + .expect_err("extends is invalid, should fail"); + + expect![[r#" + Failed to read configuration from file + ├╴extending (2): `./tests/bad.invalid-nested-extends.base.toml` -> `./tests/non-existing.toml` + ├╴extending (1): `./tests/bad.invalid-nested-extends.toml` -> `./tests/bad.invalid-nested-extends.base.toml` + │ + ├─▶ File system error + │ ╰╴file path: ./tests/non-existing.toml + │ + ╰─▶ No such file or directory (os error 2)"#]] + .assert_eq_report(&report); +} + +#[test] +fn error_reading_empty_config() { + let report = ConfigReader::new() + .with_toml_source(TomlSource::new( + PathBuf::from("./config.toml"), + toml::Table::new(), + )) + .read_and_complete::() + .expect_err("should miss required fields"); + + expect![[r#" + Some required parameters are missing + ╰╴missing parameter: `chain_id`"#]] + .assert_eq_report(&report); +} + +#[test] +fn error_extra_fields_in_multiple_files() { + let report = ConfigReader::new() + .with_toml_source(TomlSource::new( + PathBuf::from("./config.toml"), + toml! { + extra_1 = 42 + extra_2 = false + }, + )) + .with_toml_source(TomlSource::new( + PathBuf::from("./base.toml"), + toml! { + chain_id = "412" + + [torii] + bar = false + }, + )) + .read_and_complete::() + .expect_err("there are unknown fields"); + + expect![[r#" + Errors occurred while reading from file: `./base.toml` + │ + ╰─▶ Found unrecognised parameters + ╰╴unknown parameter: `torii.bar` + + Errors occurred while reading from file: `./config.toml` + │ + ╰─▶ Found unrecognised parameters + ├╴unknown parameter: `extra_1` + ╰╴unknown parameter: `extra_2`"#]] + .assert_eq_report(&report); +} + +#[test] +fn multiple_parsing_errors_in_multiple_sources() { + let report = ConfigReader::new() + .with_toml_source(TomlSource::new( + PathBuf::from("./base.toml"), + toml! { + chain_id = "ok" + torii.address = "is it socket addr?" + }, + )) + .with_toml_source(TomlSource::new( + PathBuf::from("./config.toml"), + toml! { + [torii] + address = false + }, + )) + .read_and_complete::() + .expect_err("invalid config"); + + expect![[r#" + Errors occurred while reading from file: `./base.toml` + │ + ├─▶ Failed to parse parameter `torii.address` + │ + ╰─▶ invalid socket address syntax + ╰╴value: "is it socket addr?" + + Errors occurred while reading from file: `./config.toml` + │ + ├─▶ Failed to parse parameter `torii.address` + │ + ╰─▶ invalid type: boolean `false`, expected socket address + ╰╴value: false"#]] + .assert_eq_report(&report); +} + +#[test] +fn minimal_config_ok() { + let value = ConfigReader::new() + .with_toml_source(TomlSource::new( + PathBuf::from("./config.toml"), + toml! { + chain_id = "whatever" + }, + )) + .read_and_complete::() + .expect("config is valid"); + + expect![[r#" + Root { + chain_id: "whatever", + torii: Torii { + address: WithOrigin { + value: 128.0.0.1:8080, + origin: Default { + id: ParameterId(torii.address), + }, + }, + max_content_len: 1024, + }, + kura: Kura { + store_dir: WithOrigin { + value: "./storage", + origin: Default { + id: ParameterId(kura.store_dir), + }, + }, + debug_force: false, + }, + telemetry: Telemetry { + out_file: None, + }, + logger: Logger { + level: Info, + }, + }"#]] + .assert_eq(&format!("{value:#?}")); +} + +#[test] +fn full_config_ok() { + let value = ConfigReader::new() + .with_toml_source(TomlSource::new( + PathBuf::from("./config.toml"), + toml! { + chain_id = "whatever" + + [torii] + address = "127.0.0.2:1337" + max_content_length = 19 + + [kura] + store_dir = "./my-storage" + debug_force = true + + [telemetry.dev] + out_file = "./telemetry.json" + + [logger] + level = "Error" + }, + )) + .read_and_complete::() + .expect("config is valid"); + + expect![[r#" + Root { + chain_id: "whatever", + torii: Torii { + address: WithOrigin { + value: 127.0.0.2:1337, + origin: File { + id: ParameterId(torii.address), + path: "./config.toml", + }, + }, + max_content_len: 19, + }, + kura: Kura { + store_dir: WithOrigin { + value: "./my-storage", + origin: File { + id: ParameterId(kura.store_dir), + path: "./config.toml", + }, + }, + debug_force: true, + }, + telemetry: Telemetry { + out_file: Some( + WithOrigin { + value: "./telemetry.json", + origin: File { + id: ParameterId(telemetry.dev.out_file), + path: "./config.toml", + }, + }, + ), + }, + logger: Logger { + level: Error, + }, + }"#]] + .assert_eq(&format!("{value:#?}")); +} + +#[test] +fn env_overwrites_toml() { + let root = ConfigReader::new() + .with_env(MockEnv::from(vec![("CHAIN_ID", "in env")])) + .with_toml_source(TomlSource::new( + PathBuf::from("config.toml"), + toml! { + chain_id = "in file" + }, + )) + .read_and_complete::() + .expect("config is valid"); + + assert_eq!(root.chain_id, "in env"); +} + +#[test] +#[ignore] +fn full_from_env() { + todo!() +} + +#[test] +fn multiple_env_parsing_errors() { + let report = ConfigReader::new() + .with_env(MockEnv::from([ + ("CHAIN_ID", "just to set"), + ("API_ADDRESS", "i am not socket addr"), + ("LOG_LEVEL", "error or whatever"), + ])) + .read_and_complete::() + .expect_err("invalid config"); + + expect![[r#" + Errors occurred while reading from environment variables + │ + ╰┬▶ Failed to parse parameter `torii.address` from `API_ADDRESS` + │ │ + │ ╰─▶ invalid socket address syntax + │ ╰╴value: API_ADDRESS=i am not socket addr + │ + ╰▶ Failed to parse parameter `logger.level` from `LOG_LEVEL` + │ + ╰─▶ Matching variant not found + ╰╴value: LOG_LEVEL=error or whatever"#]] + .assert_eq_report(&report); +} diff --git a/config/src/lib.rs b/config/src/lib.rs index 8776dac64e4..b43dcc16be0 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -1,9 +1,21 @@ //! Iroha configuration and related utilities. pub use iroha_config_base as base; +use stderrlog::LogLevelNum; +use tracing::log::SetLoggerError; pub mod client_api; pub mod kura; pub mod logger; pub mod parameters; pub mod snapshot; + +/// Enables tracing of configuration via [`stderrlog`]. +/// # Errors +/// See [`stderrlog::StdErrLog::init`] errors. +pub fn enable_tracing() -> Result<(), SetLoggerError> { + stderrlog::new() + .module("iroha_config_base") + .verbosity(LogLevelNum::Trace) + .init() +} diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs index c9dc0973ab6..cd921e46d0e 100644 --- a/config/src/parameters/actual.rs +++ b/config/src/parameters/actual.rs @@ -2,12 +2,13 @@ //! structures in a way that is efficient for Iroha internally. use std::{ - num::NonZeroU32, - path::{Path, PathBuf}, + num::{NonZeroU32, NonZeroUsize}, + path::PathBuf, time::Duration, }; -use iroha_config_base::{FromEnv, StdEnv, UnwrapPartial}; +use error_stack::{Result, ResultExt}; +use iroha_config_base::{read::ConfigReader, toml::TomlSource, WithOrigin}; use iroha_crypto::{KeyPair, PublicKey}; use iroha_data_model::{ metadata::Limits as MetadataLimits, peer::PeerId, transaction::TransactionLimits, ChainId, @@ -16,14 +17,11 @@ use iroha_data_model::{ use iroha_primitives::{addr::SocketAddr, unique_vec::UniqueVec}; use serde::{Deserialize, Serialize}; use url::Url; -pub use user::{DevTelemetry, Logger, Queue, Snapshot}; +pub use user::{DevTelemetry, Logger, Snapshot}; use crate::{ kura::InitMode, - parameters::{ - defaults, user, - user::{CliContext, RootPartial}, - }, + parameters::{defaults, user}, }; /// Parsed configuration root @@ -47,22 +45,23 @@ pub struct Root { pub chain_wide: ChainWide, } +/// See [`Root::from_toml_source`] +#[derive(thiserror::Error, Debug, Copy, Clone)] +#[error("Failed to read configuration from a given TOML source")] +pub struct FromTomlSourceError; + impl Root { - /// Loads configuration from a file and environment variables - /// + /// A shorthand to read config from a single provided TOML. + /// For testing purposes. /// # Errors - /// - unable to load config from a TOML file - /// - unable to parse config from envs - /// - the config is invalid - pub fn load>(path: Option

, cli: CliContext) -> Result { - let from_file = path.map(RootPartial::from_toml).transpose()?; - let from_env = RootPartial::from_env(&StdEnv)?; - let merged = match from_file { - Some(x) => x.merge(from_env), - None => from_env, - }; - let config = merged.unwrap_partial()?.parse(cli)?; - Ok(config) + /// If config reading/parsing fails. + pub fn from_toml_source(src: TomlSource) -> Result { + ConfigReader::new() + .with_toml_source(src) + .read_and_complete::() + .change_context(FromTomlSourceError)? + .parse() + .change_context(FromTomlSourceError) } } @@ -86,7 +85,7 @@ impl Common { #[allow(missing_docs)] #[derive(Debug, Clone)] pub struct Network { - pub address: SocketAddr, + pub address: WithOrigin, pub idle_timeout: Duration, } @@ -102,8 +101,8 @@ pub enum Genesis { Full { /// Genesis account key pair key_pair: KeyPair, - /// Path to the [`RawGenesisBlock`] - file: PathBuf, + /// Path to `RawGenesisBlock` + file: WithOrigin, }, } @@ -125,21 +124,30 @@ impl Genesis { } } +#[allow(missing_docs)] +#[derive(Debug, Clone, Copy)] +pub struct Queue { + pub capacity: NonZeroUsize, + pub capacity_per_user: NonZeroUsize, + pub transaction_time_to_live: Duration, + pub future_threshold: Duration, +} + #[allow(missing_docs)] #[derive(Debug, Clone)] pub struct Kura { pub init_mode: InitMode, - pub store_dir: PathBuf, + pub store_dir: WithOrigin, pub debug_output_new_blocks: bool, } impl Default for Queue { fn default() -> Self { Self { - transaction_time_to_live: defaults::queue::DEFAULT_TRANSACTION_TIME_TO_LIVE, - future_threshold: defaults::queue::DEFAULT_FUTURE_THRESHOLD, - capacity: defaults::queue::DEFAULT_MAX_TRANSACTIONS_IN_QUEUE, - capacity_per_user: defaults::queue::DEFAULT_MAX_TRANSACTIONS_IN_QUEUE_PER_USER, + transaction_time_to_live: defaults::queue::TRANSACTION_TIME_TO_LIVE, + future_threshold: defaults::queue::FUTURE_THRESHOLD, + capacity: defaults::queue::CAPACITY, + capacity_per_user: defaults::queue::CAPACITY_PER_USER, } } } @@ -147,10 +155,32 @@ impl Default for Queue { #[derive(Debug, Clone)] #[allow(missing_docs)] pub struct Sumeragi { - pub trusted_peers: UniqueVec, + pub trusted_peers: WithOrigin, pub debug_force_soft_fork: bool, } +#[derive(Debug, Clone)] +#[allow(missing_docs)] +pub struct TrustedPeers { + pub myself: PeerId, + pub others: UniqueVec, +} + +impl TrustedPeers { + /// Returns a list of trusted peers which is guaranteed to have at + /// least one element - the id of the peer itself. + pub fn into_non_empty_vec(self) -> UniqueVec { + std::iter::once(self.myself).chain(self.others).collect() + } +} + +impl Sumeragi { + /// Tells whether a trusted peers list has some other peers except for the peer itself + pub fn contains_other_trusted_peers(&self) -> bool { + self.trusted_peers.value().others.len() > 1 + } +} + #[derive(Debug, Clone, Copy)] #[allow(missing_docs)] pub struct LiveQueryStore { @@ -160,7 +190,7 @@ pub struct LiveQueryStore { impl Default for LiveQueryStore { fn default() -> Self { Self { - idle_time: defaults::torii::DEFAULT_QUERY_IDLE_TIME, + idle_time: defaults::torii::QUERY_IDLE_TIME, } } } @@ -206,16 +236,16 @@ impl ChainWide { impl Default for ChainWide { fn default() -> Self { Self { - max_transactions_in_block: defaults::chain_wide::DEFAULT_MAX_TXS, - block_time: defaults::chain_wide::DEFAULT_BLOCK_TIME, - commit_time: defaults::chain_wide::DEFAULT_COMMIT_TIME, - transaction_limits: defaults::chain_wide::DEFAULT_TRANSACTION_LIMITS, - domain_metadata_limits: defaults::chain_wide::DEFAULT_METADATA_LIMITS, - account_metadata_limits: defaults::chain_wide::DEFAULT_METADATA_LIMITS, - asset_definition_metadata_limits: defaults::chain_wide::DEFAULT_METADATA_LIMITS, - asset_metadata_limits: defaults::chain_wide::DEFAULT_METADATA_LIMITS, - trigger_metadata_limits: defaults::chain_wide::DEFAULT_METADATA_LIMITS, - ident_length_limits: defaults::chain_wide::DEFAULT_IDENT_LENGTH_LIMITS, + max_transactions_in_block: defaults::chain_wide::MAX_TXS, + block_time: defaults::chain_wide::BLOCK_TIME, + commit_time: defaults::chain_wide::COMMIT_TIME, + transaction_limits: defaults::chain_wide::TRANSACTION_LIMITS, + domain_metadata_limits: defaults::chain_wide::METADATA_LIMITS, + account_metadata_limits: defaults::chain_wide::METADATA_LIMITS, + asset_definition_metadata_limits: defaults::chain_wide::METADATA_LIMITS, + asset_metadata_limits: defaults::chain_wide::METADATA_LIMITS, + trigger_metadata_limits: defaults::chain_wide::METADATA_LIMITS, + ident_length_limits: defaults::chain_wide::IDENT_LENGTH_LIMITS, executor_runtime: WasmRuntime::default(), wasm_runtime: WasmRuntime::default(), } @@ -233,8 +263,8 @@ pub struct WasmRuntime { impl Default for WasmRuntime { fn default() -> Self { Self { - fuel_limit: defaults::chain_wide::DEFAULT_WASM_FUEL_LIMIT, - max_memory_bytes: defaults::chain_wide::DEFAULT_WASM_MAX_MEMORY_BYTES, + fuel_limit: defaults::chain_wide::WASM_FUEL_LIMIT, + max_memory_bytes: defaults::chain_wide::WASM_MAX_MEMORY_BYTES, } } } @@ -242,7 +272,7 @@ impl Default for WasmRuntime { #[derive(Debug, Clone)] #[allow(missing_docs)] pub struct Torii { - pub address: SocketAddr, + pub address: WithOrigin, pub max_content_len_bytes: u64, } diff --git a/config/src/parameters/defaults.rs b/config/src/parameters/defaults.rs index 4b42ac46c90..59f86625c8c 100644 --- a/config/src/parameters/defaults.rs +++ b/config/src/parameters/defaults.rs @@ -14,51 +14,50 @@ use nonzero_ext::nonzero; pub mod queue { use super::*; - pub const DEFAULT_MAX_TRANSACTIONS_IN_QUEUE: NonZeroUsize = nonzero!(2_usize.pow(16)); - pub const DEFAULT_MAX_TRANSACTIONS_IN_QUEUE_PER_USER: NonZeroUsize = nonzero!(2_usize.pow(16)); + pub const CAPACITY: NonZeroUsize = nonzero!(2_usize.pow(16)); + pub const CAPACITY_PER_USER: NonZeroUsize = nonzero!(2_usize.pow(16)); // 24 hours - pub const DEFAULT_TRANSACTION_TIME_TO_LIVE: Duration = Duration::from_secs(24 * 60 * 60); - pub const DEFAULT_FUTURE_THRESHOLD: Duration = Duration::from_secs(1); + pub const TRANSACTION_TIME_TO_LIVE: Duration = Duration::from_secs(24 * 60 * 60); + pub const FUTURE_THRESHOLD: Duration = Duration::from_secs(1); } + pub mod kura { - pub const DEFAULT_STORE_DIR: &str = "./storage"; + pub const STORE_DIR: &str = "./storage"; } pub mod network { use super::*; - pub const DEFAULT_TRANSACTION_GOSSIP_PERIOD: Duration = Duration::from_secs(1); - - pub const DEFAULT_BLOCK_GOSSIP_PERIOD: Duration = Duration::from_secs(10); + pub const TRANSACTION_GOSSIP_PERIOD: Duration = Duration::from_secs(1); + pub const TRANSACTION_GOSSIP_MAX_SIZE: NonZeroU32 = nonzero!(500u32); - pub const DEFAULT_MAX_TRANSACTIONS_PER_GOSSIP: NonZeroU32 = nonzero!(500u32); - pub const DEFAULT_MAX_BLOCKS_PER_GOSSIP: NonZeroU32 = nonzero!(4u32); + pub const BLOCK_GOSSIP_PERIOD: Duration = Duration::from_secs(10); + pub const BLOCK_GOSSIP_MAX_SIZE: NonZeroU32 = nonzero!(4u32); - pub const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(60); + pub const IDLE_TIMEOUT: Duration = Duration::from_secs(60); } pub mod snapshot { use super::*; - pub const DEFAULT_STORE_DIR: &str = "./storage/snapshot"; + pub const STORE_DIR: &str = "./storage/snapshot"; // The default frequency of making snapshots is 1 minute, need to be adjusted for larger world state view size - pub const DEFAULT_CREATE_EVERY: Duration = Duration::from_secs(60); + pub const CREATE_EVERY: Duration = Duration::from_secs(60); } pub mod chain_wide { - use super::*; - pub const DEFAULT_MAX_TXS: NonZeroU32 = nonzero!(2_u32.pow(9)); - pub const DEFAULT_BLOCK_TIME: Duration = Duration::from_secs(2); - pub const DEFAULT_COMMIT_TIME: Duration = Duration::from_secs(4); - pub const DEFAULT_WASM_FUEL_LIMIT: u64 = 55_000_000; + pub const MAX_TXS: NonZeroU32 = nonzero!(2_u32.pow(9)); + pub const BLOCK_TIME: Duration = Duration::from_secs(2); + pub const COMMIT_TIME: Duration = Duration::from_secs(4); + pub const WASM_FUEL_LIMIT: u64 = 55_000_000; // TODO: wrap into a `Bytes` newtype - pub const DEFAULT_WASM_MAX_MEMORY_BYTES: u32 = 500 * 2_u32.pow(20); + pub const WASM_MAX_MEMORY_BYTES: u32 = 500 * 2_u32.pow(20); /// Default estimation of consensus duration. - pub const DEFAULT_CONSENSUS_ESTIMATION: Duration = - match DEFAULT_BLOCK_TIME.checked_add(match DEFAULT_COMMIT_TIME.checked_div(2) { + pub const CONSENSUS_ESTIMATION: Duration = + match BLOCK_TIME.checked_add(match COMMIT_TIME.checked_div(2) { Some(x) => x, None => unreachable!(), }) { @@ -67,32 +66,31 @@ pub mod chain_wide { }; /// Default limits for metadata - pub const DEFAULT_METADATA_LIMITS: MetadataLimits = - MetadataLimits::new(2_u32.pow(20), 2_u32.pow(12)); + pub const METADATA_LIMITS: MetadataLimits = MetadataLimits::new(2_u32.pow(20), 2_u32.pow(12)); /// Default limits for ident length - pub const DEFAULT_IDENT_LENGTH_LIMITS: LengthLimits = LengthLimits::new(1, 2_u32.pow(7)); + pub const IDENT_LENGTH_LIMITS: LengthLimits = LengthLimits::new(1, 2_u32.pow(7)); /// Default maximum number of instructions and expressions per transaction - pub const DEFAULT_MAX_INSTRUCTION_NUMBER: u64 = 2_u64.pow(12); + pub const MAX_INSTRUCTION_NUMBER: u64 = 2_u64.pow(12); /// Default maximum number of instructions and expressions per transaction - pub const DEFAULT_MAX_WASM_SIZE_BYTES: u64 = 4 * 2_u64.pow(20); + pub const MAX_WASM_SIZE_BYTES: u64 = 4 * 2_u64.pow(20); /// Default transaction limits - pub const DEFAULT_TRANSACTION_LIMITS: TransactionLimits = - TransactionLimits::new(DEFAULT_MAX_INSTRUCTION_NUMBER, DEFAULT_MAX_WASM_SIZE_BYTES); + pub const TRANSACTION_LIMITS: TransactionLimits = + TransactionLimits::new(MAX_INSTRUCTION_NUMBER, MAX_WASM_SIZE_BYTES); } pub mod torii { use std::time::Duration; - pub const DEFAULT_MAX_CONTENT_LENGTH: u64 = 2_u64.pow(20) * 16; - pub const DEFAULT_QUERY_IDLE_TIME: Duration = Duration::from_secs(30); + pub const MAX_CONTENT_LENGTH: u64 = 2_u64.pow(20) * 16; + pub const QUERY_IDLE_TIME: Duration = Duration::from_secs(30); } pub mod telemetry { use std::time::Duration; /// Default minimal retry period - pub const DEFAULT_MIN_RETRY_PERIOD: Duration = Duration::from_secs(1); + pub const MIN_RETRY_PERIOD: Duration = Duration::from_secs(1); /// Default maximum exponent for the retry delay - pub const DEFAULT_MAX_RETRY_DELAY_EXPONENT: u8 = 4; + pub const MAX_RETRY_DELAY_EXPONENT: u8 = 4; } diff --git a/config/src/parameters/user.rs b/config/src/parameters/user.rs index 1a3c66f2026..fbcd718eee4 100644 --- a/config/src/parameters/user.rs +++ b/config/src/parameters/user.rs @@ -10,124 +10,87 @@ #![allow(missing_docs)] use std::{ + borrow::Cow, + convert::Infallible, fmt::Debug, - fs::File, - io::Read, num::{NonZeroU32, NonZeroUsize}, - path::{Path, PathBuf}, - time::Duration, + path::PathBuf, }; -pub use boilerplate::*; -use eyre::{eyre, Report, WrapErr}; -use iroha_config_base::{Emitter, ErrorsCollection, HumanBytes, Merge}; -use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; +use error_stack::{Result, ResultExt}; +use iroha_config_base::{ + attach::ConfigValueAndOrigin, + env::FromEnvStr, + util::{Emitter, EmitterResultExt, HumanBytes, HumanDuration}, + ReadConfig, WithOrigin, +}; +use iroha_crypto::{PrivateKey, PublicKey}; use iroha_data_model::{ metadata::Limits as MetadataLimits, peer::PeerId, transaction::TransactionLimits, ChainId, LengthLimits, Level, }; use iroha_primitives::{addr::SocketAddr, unique_vec::UniqueVec}; +use serde::Deserialize; use url::Url; use crate::{ kura::InitMode as KuraInitMode, logger::Format as LoggerFormat, - parameters::{actual, defaults::telemetry::*}, + parameters::{actual, defaults}, snapshot::Mode as SnapshotMode, }; -mod boilerplate; +#[derive(Deserialize, Debug)] +struct ChainIdInConfig(ChainId); + +impl FromEnvStr for ChainIdInConfig { + type Error = Infallible; -#[derive(Debug)] + fn from_env_str(value: Cow<'_, str>) -> std::result::Result + where + Self: Sized, + { + Ok(Self(ChainId::from(value))) + } +} + +#[derive(Debug, ReadConfig)] pub struct Root { - chain_id: ChainId, - public_key: PublicKey, - private_key: PrivateKey, + #[config(env = "CHAIN_ID")] + chain_id: ChainIdInConfig, + #[config(env = "PUBLIC_KEY")] + public_key: WithOrigin, + #[config(env = "PRIVATE_KEY")] + private_key: WithOrigin, + #[config(nested)] genesis: Genesis, + #[config(nested)] kura: Kura, + #[config(nested)] sumeragi: Sumeragi, + #[config(nested)] network: Network, + #[config(nested)] logger: Logger, + #[config(nested)] queue: Queue, + #[config(nested)] snapshot: Snapshot, - telemetry: Telemetry, + telemetry: Option, + #[config(nested)] dev_telemetry: DevTelemetry, + #[config(nested)] torii: Torii, + #[config(nested)] chain_wide: ChainWide, } -impl RootPartial { - /// Read the partial from TOML file - /// - /// # Errors - /// - If file is not found, or not a valid TOML - /// - If failed to parse data into a layer - /// - If failed to read other configurations specified in `extends` - pub fn from_toml(path: impl AsRef) -> eyre::Result { - let contents = { - let mut file = File::open(path.as_ref()).wrap_err_with(|| { - eyre!("cannot open file at location `{}`", path.as_ref().display()) - })?; - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - contents - }; - let mut layer: Self = toml::from_str(&contents).wrap_err("failed to parse toml")?; - - let base_path = path - .as_ref() - .parent() - .expect("the config file path could not be empty or root"); - - layer.normalise_paths(base_path); - - if let Some(paths) = layer.extends.take() { - let base = paths - .iter() - .try_fold(None, |acc: Option, extends_path| { - // extends path is not normalised relative to the config file yet - let full_path = base_path.join(extends_path); - - let base = Self::from_toml(&full_path) - .wrap_err_with(|| eyre!("cannot extend from `{}`", full_path.display()))?; - - match acc { - None => Ok::, Report>(Some(base)), - Some(other_base) => Ok(Some(other_base.merge(base))), - } - })?; - if let Some(base) = base { - layer = base.merge(layer) - }; - } - - Ok(layer) - } - - /// **Note:** this function doesn't affect `extends` - fn normalise_paths(&mut self, relative_to: impl AsRef) { - let path = relative_to.as_ref(); - - macro_rules! patch { - ($value:expr) => { - $value.as_mut().map(|x| { - *x = path.join(&x); - }) - }; - } - - patch!(self.genesis.file); - patch!(self.snapshot.store_dir); - patch!(self.kura.store_dir); - patch!(self.dev_telemetry.out_file); - } - - // FIXME workaround the inconvenient way `Merge::merge` works - #[must_use] - pub fn merge(mut self, other: Self) -> Self { - Merge::merge(&mut self, other); - self - } +#[derive(thiserror::Error, Debug, Copy, Clone)] +pub enum ParseError { + #[error("Failed to construct the key pair")] + BadKeyPair, + #[error("Invalid genesis configuration")] + BadGenesis, } impl Root { @@ -135,109 +98,55 @@ impl Root { /// /// # Errors /// If any invalidity found. - pub fn parse(self, cli: CliContext) -> Result> { + #[allow(clippy::too_many_lines)] + pub fn parse(self) -> Result { let mut emitter = Emitter::new(); - let key_pair = - KeyPair::new(self.public_key, self.private_key) - .wrap_err("failed to construct a key pair from `iroha.public_key` and `iroha.private_key` configuration parameters") - .map_or_else(|err| { - emitter.emit(err); - None - }, Some); - - let genesis = self.genesis.parse(cli).map_or_else( - |err| { - // FIXME - emitter.emit(eyre!("{err}")); - None - }, - Some, - ); + let (private_key, private_key_origin) = self.private_key.into_tuple(); + let (public_key, public_key_origin) = self.public_key.into_tuple(); + let key_pair = iroha_crypto::KeyPair::new(public_key, private_key) + .attach_printable(ConfigValueAndOrigin::new("[REDACTED]", public_key_origin)) + .attach_printable(ConfigValueAndOrigin::new("[REDACTED]", private_key_origin)) + .change_context(ParseError::BadKeyPair) + .ok_or_emit(&mut emitter); - // TODO: enable this check after fix of https://github.com/hyperledger/iroha/issues/4383 - // if let Some(actual::Genesis::Full { file, .. }) = &genesis { - // if !file.is_file() { - // emitter.emit(eyre!("unable to access `genesis.file`: {}", file.display())) - // } - // } + let genesis = self + .genesis + .parse() + .change_context(ParseError::BadGenesis) + .ok_or_emit(&mut emitter); let kura = self.kura.parse(); - validate_directory_path(&mut emitter, &kura.store_dir, "kura.store_dir"); - - let sumeragi = self.sumeragi.parse().map_or_else( - |err| { - emitter.emit(err); - None - }, - Some, - ); - - if let Some(ref config) = sumeragi { - if !cli.submit_genesis && config.trusted_peers.len() == 0 { - emitter.emit(eyre!("\ - The network consists from this one peer only (no `sumeragi.trusted_peers` provided). \ - Since `--submit-genesis` is not set, there is no way to receive the genesis block. \ - Either provide the genesis by setting `--submit-genesis` argument, `genesis.private_key`, \ - and `genesis.file` configuration parameters, or increase the number of trusted peers in \ - the network using `sumeragi.trusted_peers` configuration parameter.\ - ")); - } - } let (network, block_sync, transaction_gossiper) = self.network.parse(); - let logger = self.logger; let queue = self.queue; - let snapshot = self.snapshot; - validate_directory_path(&mut emitter, &snapshot.store_dir, "snapshot.store_dir"); - let dev_telemetry = self.dev_telemetry; - if let Some(path) = &dev_telemetry.out_file { - if path.parent().is_none() { - emitter.emit(eyre!("`dev_telemetry.out_file` is not a valid file path")) - } - if path.is_dir() { - emitter.emit(eyre!("`dev_telemetry.out_file` is expected to be a file path, but it is a directory: {}", path.display())) - } - } - let (torii, live_query_store) = self.torii.parse(); - - let telemetry = self.telemetry.parse().map_or_else( - |err| { - emitter.emit(err); - None - }, - Some, - ); - + let telemetry = self.telemetry.map(actual::Telemetry::from); let chain_wide = self.chain_wide.parse(); - if network.address == torii.address { - emitter.emit(eyre!( - "`iroha.p2p_address` and `torii.address` should not be the same" - )) - } + let peer_id = key_pair.as_ref().map(|key_pair| { + PeerId::new( + network.address.value().clone(), + key_pair.public_key().clone(), + ) + }); + + let sumeragi = peer_id + .as_ref() + .map(|id| self.sumeragi.parse_and_push_self(id.clone())); - emitter.finish()?; + emitter.into_result()?; let key_pair = key_pair.unwrap(); - let peer_id = PeerId::new(network.address.clone(), key_pair.public_key().clone()); - let peer = actual::Common { - chain_id: self.chain_id, + chain_id: self.chain_id.0, key_pair, - peer_id, + peer_id: peer_id.unwrap(), }; - let telemetry = telemetry.unwrap(); let genesis = genesis.unwrap(); - let sumeragi = { - let mut x = sumeragi.unwrap(); - x.trusted_peers.push(peer.peer_id()); - x - }; Ok(actual::Root { common: peer, @@ -245,12 +154,12 @@ impl Root { genesis, torii, kura, - sumeragi, + sumeragi: sumeragi.unwrap(), block_sync, transaction_gossiper, live_query_store, logger, - queue, + queue: queue.parse(), snapshot, telemetry, dev_telemetry, @@ -259,66 +168,60 @@ impl Root { } } -fn validate_directory_path( - emitter: &mut Emitter, - path: impl AsRef, - name: impl AsRef, -) { - if path.as_ref().is_file() { - emitter.emit(eyre!( - "`{}` is expected to be a directory path (existing or non-existing), but it points to an existing file: {}", - name.as_ref(), - path.as_ref().display() - )) - } -} - -#[derive(Copy, Clone)] -pub struct CliContext { - pub submit_genesis: bool, -} - -#[derive(Debug)] +#[derive(Debug, ReadConfig)] pub struct Genesis { - pub public_key: PublicKey, - pub private_key: Option, - pub file: Option, + #[config(env = "GENESIS_PUBLIC_KEY")] + pub public_key: WithOrigin, + #[config(env = "GENESIS_PRIVATE_KEY")] + pub private_key: Option>, + #[config(env = "GENESIS_FILE")] + pub file: Option>, } impl Genesis { - fn parse(self, cli: CliContext) -> Result { - match (self.private_key, self.file, cli.submit_genesis) { - (None, None, false) => Ok(actual::Genesis::Partial { - public_key: self.public_key, - }), - (Some(private_key), Some(file), true) => Ok(actual::Genesis::Full { - key_pair: KeyPair::new(self.public_key, private_key) - .map_err(GenesisConfigError::from)?, - file, + fn parse(self) -> Result { + match (self.private_key, self.file) { + (None, None) => Ok(actual::Genesis::Partial { + public_key: self.public_key.into_value(), }), - (Some(_), Some(_), false) => Err(GenesisConfigError::GenesisWithoutSubmit), - (None, None, true) => Err(GenesisConfigError::SubmitWithoutGenesis), - _ => Err(GenesisConfigError::Inconsistent), + (Some(private_key), Some(file)) => { + let (private_key, priv_key_origin) = private_key.into_tuple(); + let (public_key, pub_key_origin) = self.public_key.into_tuple(); + let key_pair = iroha_crypto::KeyPair::new(public_key, private_key) + .attach_printable(ConfigValueAndOrigin::new("[REDACTED]", pub_key_origin)) + .attach_printable(ConfigValueAndOrigin::new("[REDACTED]", priv_key_origin)) + .change_context(GenesisConfigError::KeyPair)?; + Ok(actual::Genesis::Full { key_pair, file }) + } + (key, _) => { + Err(GenesisConfigError::Inconsistent).attach_printable(if key.is_some() { + "`genesis.private_key` is set, but `genesis.file` is not" + } else { + "`genesis.file` is set, but `genesis.private_key` is not" + })? + } } } } -#[derive(Debug, displaydoc::Display, thiserror::Error)] +#[derive(Debug, displaydoc::Display, thiserror::Error, Copy, Clone)] pub enum GenesisConfigError { - /// `genesis.file` and `genesis.private_key` are presented, but `--submit-genesis` was not set - GenesisWithoutSubmit, - /// `--submit-genesis` was set, but `genesis.file` and `genesis.private_key` are not presented - SubmitWithoutGenesis, - /// `genesis.file` and `genesis.private_key` should be set together + /// Invalid combination of provided parameters Inconsistent, - /// failed to construct the genesis's keypair using `genesis.public_key` and `genesis.private_key` configuration parameters - KeyPair(#[from] iroha_crypto::error::Error), + /// failed to construct the genesis's keypair from public and private keys + KeyPair, } -#[derive(Debug)] +#[derive(Debug, ReadConfig)] pub struct Kura { + #[config(env = "KURA_INIT_MODE", default)] pub init_mode: KuraInitMode, - pub store_dir: PathBuf, + #[config( + env = "KURA_STORE_DIR", + default = "PathBuf::from(defaults::kura::STORE_DIR)" + )] + pub store_dir: WithOrigin, + #[config(nested)] pub debug: KuraDebug, } @@ -326,7 +229,7 @@ impl Kura { fn parse(self) -> actual::Kura { let Self { init_mode, - store_dir: block_store_path, + store_dir, debug: KuraDebug { output_new_blocks: debug_output_new_blocks, @@ -335,68 +238,85 @@ impl Kura { actual::Kura { init_mode, - store_dir: block_store_path, + store_dir, debug_output_new_blocks, } } } -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, ReadConfig)] pub struct KuraDebug { + #[config(env = "KURA_DEBUG_OUTPUT_NEW_BLOCKS", default)] output_new_blocks: bool, } -#[derive(Debug)] +#[derive(Debug, ReadConfig)] pub struct Sumeragi { - pub trusted_peers: Option>, + #[config(env = "SUMERAGI_TRUSTED_PEERS", default)] + pub trusted_peers: WithOrigin, + #[config(nested)] pub debug: SumeragiDebug, } +#[derive(Debug, Deserialize)] +pub struct TrustedPeers(UniqueVec); + +impl FromEnvStr for TrustedPeers { + type Error = json5::Error; + + fn from_env_str(value: Cow<'_, str>) -> std::result::Result + where + Self: Sized, + { + Ok(Self(json5::from_str(value.as_ref())?)) + } +} + +impl Default for TrustedPeers { + fn default() -> Self { + Self(UniqueVec::new()) + } +} + impl Sumeragi { - fn parse(self) -> Result { + fn parse_and_push_self(self, self_id: PeerId) -> actual::Sumeragi { let Self { trusted_peers, debug: SumeragiDebug { force_soft_fork }, } = self; - let trusted_peers = construct_unique_vec(trusted_peers.unwrap_or(vec![]))?; - - Ok(actual::Sumeragi { - trusted_peers, + actual::Sumeragi { + trusted_peers: trusted_peers.map(|x| actual::TrustedPeers { + myself: self_id, + others: x.0, + }), debug_force_soft_fork: force_soft_fork, - }) + } } } -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, ReadConfig)] pub struct SumeragiDebug { + #[config(default)] pub force_soft_fork: bool, } -// FIXME: handle duplicates properly, not here, and with details -fn construct_unique_vec( - unchecked: Vec, -) -> Result, eyre::Report> { - let mut unique = UniqueVec::new(); - for x in unchecked { - let pushed = unique.push(x); - if !pushed { - Err(eyre!("found duplicate"))? - } - } - Ok(unique) -} - -#[derive(Debug, Clone)] +#[derive(Debug, Clone, ReadConfig)] pub struct Network { /// Peer-to-peer address - pub address: SocketAddr, + #[config(env = "P2P_ADDRESS")] + pub address: WithOrigin, + #[config(default = "defaults::network::BLOCK_GOSSIP_MAX_SIZE")] pub block_gossip_max_size: NonZeroU32, - pub block_gossip_period: Duration, + #[config(default = "defaults::network::BLOCK_GOSSIP_PERIOD.into()")] + pub block_gossip_period: HumanDuration, + #[config(default = "defaults::network::TRANSACTION_GOSSIP_MAX_SIZE")] pub transaction_gossip_max_size: NonZeroU32, - pub transaction_gossip_period: Duration, + #[config(default = "defaults::network::TRANSACTION_GOSSIP_PERIOD.into()")] + pub transaction_gossip_period: HumanDuration, /// Duration of time after which connection with peer is terminated if peer is idle - pub idle_timeout: Duration, + #[config(default = "defaults::network::IDLE_TIMEOUT.into()")] + pub idle_timeout: HumanDuration, } impl Network { @@ -419,113 +339,165 @@ impl Network { ( actual::Network { address, - idle_timeout, + idle_timeout: idle_timeout.get(), }, actual::BlockSync { - gossip_period: block_gossip_period, + gossip_period: block_gossip_period.get(), gossip_max_size: block_gossip_max_size, }, actual::TransactionGossiper { - gossip_period: transaction_gossip_period, + gossip_period: transaction_gossip_period.get(), gossip_max_size: transaction_gossip_max_size, }, ) } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, ReadConfig)] pub struct Queue { /// The upper limit of the number of transactions waiting in the queue. + #[config(default = "defaults::queue::CAPACITY")] pub capacity: NonZeroUsize, - /// The upper limit of the number of transactions waiting in the queue for single user. + /// The upper limit of the number of transactions waiting in the queue for a single user. /// Use this option to apply throttling. + #[config(default = "defaults::queue::CAPACITY_PER_USER")] pub capacity_per_user: NonZeroUsize, /// The transaction will be dropped after this time if it is still in the queue. - pub transaction_time_to_live: Duration, + #[config(default = "defaults::queue::TRANSACTION_TIME_TO_LIVE.into()")] + pub transaction_time_to_live: HumanDuration, /// The threshold to determine if a transaction has been tampered to have a future timestamp. - pub future_threshold: Duration, + #[config(default = "defaults::queue::FUTURE_THRESHOLD.into()")] + pub future_threshold: HumanDuration, } -#[derive(Debug, Clone, Copy, Default)] +impl Queue { + pub fn parse(self) -> actual::Queue { + let Self { + capacity, + capacity_per_user, + transaction_time_to_live, + future_threshold, + } = self; + actual::Queue { + capacity, + capacity_per_user, + transaction_time_to_live: transaction_time_to_live.0, + future_threshold: future_threshold.0, + } + } +} + +#[derive(Debug, Clone, Copy, Default, ReadConfig)] pub struct Logger { /// Level of logging verbosity // TODO: parse user provided value in a case insensitive way, // because `format` is set in lowercase, and `LOG_LEVEL=INFO` + `LOG_FORMAT=pretty` // looks inconsistent + #[config(env = "LOG_LEVEL", default)] pub level: Level, /// Output format + #[config(env = "LOG_FORMAT", default)] pub format: LoggerFormat, } -#[derive(Debug)] +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Telemetry { // Fields here are Options so that it is possible to warn the user if e.g. they provided `min_retry_period`, but haven't // provided `name` and `url` - pub name: Option, - pub url: Option, - pub min_retry_period: Option, - pub max_retry_delay_exponent: Option, + name: String, + url: Url, + #[serde(default)] + min_retry_period: TelemetryMinRetryPeriod, + #[serde(default)] + max_retry_delay_exponent: TelemetryMaxRetryDelayExponent, } -#[derive(Debug, Clone)] -pub struct DevTelemetry { - pub out_file: Option, +#[derive(Deserialize, Debug, Copy, Clone)] +struct TelemetryMinRetryPeriod(HumanDuration); + +impl Default for TelemetryMinRetryPeriod { + fn default() -> Self { + Self(HumanDuration(defaults::telemetry::MIN_RETRY_PERIOD)) + } } -impl Telemetry { - fn parse(self) -> Result, Report> { - let Self { +#[derive(Deserialize, Debug, Copy, Clone)] +struct TelemetryMaxRetryDelayExponent(u8); + +impl Default for TelemetryMaxRetryDelayExponent { + fn default() -> Self { + Self(defaults::telemetry::MAX_RETRY_DELAY_EXPONENT) + } +} + +impl From for actual::Telemetry { + fn from( + Telemetry { + name, + url, + min_retry_period: TelemetryMinRetryPeriod(HumanDuration(min_retry_period)), + max_retry_delay_exponent: TelemetryMaxRetryDelayExponent(max_retry_delay_exponent), + }: Telemetry, + ) -> Self { + Self { name, url, - max_retry_delay_exponent, min_retry_period, - } = self; - - let regular = match (name, url) { - (Some(name), Some(url)) => Some(actual::Telemetry { - name, - url, - max_retry_delay_exponent: max_retry_delay_exponent - .unwrap_or(DEFAULT_MAX_RETRY_DELAY_EXPONENT), - min_retry_period: min_retry_period.unwrap_or(DEFAULT_MIN_RETRY_PERIOD), - }), - // TODO warn user if they provided retry parameters while not providing essential ones - (None, None) => None, - _ => { - // TODO improve error detail - return Err(eyre!( - "telemetry.name and telemetry.file should be set together" - ))?; - } - }; - - Ok(regular) + max_retry_delay_exponent, + } } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, ReadConfig)] +pub struct DevTelemetry { + pub out_file: Option>, +} + +#[derive(Debug, Clone, ReadConfig)] pub struct Snapshot { + #[config(default, env = "SNAPSHOT_MODE")] pub mode: SnapshotMode, - pub create_every: Duration, - pub store_dir: PathBuf, + #[config(default = "defaults::snapshot::CREATE_EVERY.into()")] + pub create_every: HumanDuration, + #[config( + default = "PathBuf::from(defaults::snapshot::STORE_DIR)", + env = "SNAPSHOT_STORE_DIR" + )] + pub store_dir: WithOrigin, } -#[derive(Debug, Copy, Clone)] +// TODO: make serde +#[derive(Debug, Copy, Clone, ReadConfig)] pub struct ChainWide { + #[config(default = "defaults::chain_wide::MAX_TXS")] pub max_transactions_in_block: NonZeroU32, - pub block_time: Duration, - pub commit_time: Duration, + #[config(default = "defaults::chain_wide::BLOCK_TIME.into()")] + pub block_time: HumanDuration, + #[config(default = "defaults::chain_wide::COMMIT_TIME.into()")] + pub commit_time: HumanDuration, + #[config(default = "defaults::chain_wide::TRANSACTION_LIMITS")] pub transaction_limits: TransactionLimits, + #[config(default = "defaults::chain_wide::METADATA_LIMITS")] pub domain_metadata_limits: MetadataLimits, + #[config(default = "defaults::chain_wide::METADATA_LIMITS")] pub asset_definition_metadata_limits: MetadataLimits, + #[config(default = "defaults::chain_wide::METADATA_LIMITS")] pub account_metadata_limits: MetadataLimits, + #[config(default = "defaults::chain_wide::METADATA_LIMITS")] pub asset_metadata_limits: MetadataLimits, + #[config(default = "defaults::chain_wide::METADATA_LIMITS")] pub trigger_metadata_limits: MetadataLimits, + #[config(default = "defaults::chain_wide::IDENT_LENGTH_LIMITS")] pub ident_length_limits: LengthLimits, + #[config(default = "defaults::chain_wide::WASM_FUEL_LIMIT")] pub executor_fuel_limit: u64, - pub executor_max_memory: HumanBytes, + #[config(default = "defaults::chain_wide::WASM_MAX_MEMORY_BYTES")] + pub executor_max_memory: u32, + #[config(default = "defaults::chain_wide::WASM_FUEL_LIMIT")] pub wasm_fuel_limit: u64, - pub wasm_max_memory: HumanBytes, + #[config(default = "defaults::chain_wide::WASM_MAX_MEMORY_BYTES")] + pub wasm_max_memory: u32, } impl ChainWide { @@ -549,8 +521,8 @@ impl ChainWide { actual::ChainWide { max_transactions_in_block, - block_time, - commit_time, + block_time: block_time.get(), + commit_time: commit_time.get(), transaction_limits, asset_metadata_limits, trigger_metadata_limits, @@ -560,82 +532,37 @@ impl ChainWide { ident_length_limits, executor_runtime: actual::WasmRuntime { fuel_limit: executor_fuel_limit, - max_memory_bytes: executor_max_memory.get(), + max_memory_bytes: executor_max_memory, }, wasm_runtime: actual::WasmRuntime { fuel_limit: wasm_fuel_limit, - max_memory_bytes: wasm_max_memory.get(), + max_memory_bytes: wasm_max_memory, }, } } } -#[derive(Debug)] +#[derive(Debug, ReadConfig)] pub struct Torii { - pub address: SocketAddr, - pub max_content_len: HumanBytes, - pub query_idle_time: Duration, + #[config(env = "API_ADDRESS")] + pub address: WithOrigin, + #[config(default = "defaults::torii::MAX_CONTENT_LENGTH.into()")] + pub max_content_length: HumanBytes, + #[config(default = "defaults::torii::QUERY_IDLE_TIME.into()")] + pub query_idle_time: HumanDuration, } impl Torii { fn parse(self) -> (actual::Torii, actual::LiveQueryStore) { let torii = actual::Torii { address: self.address, - max_content_len_bytes: self.max_content_len.get(), + max_content_len_bytes: self.max_content_length.get(), }; let query = actual::LiveQueryStore { - idle_time: self.query_idle_time, + idle_time: self.query_idle_time.get(), }; (torii, query) } } - -#[cfg(test)] -mod tests { - use iroha_config_base::{FromEnv, TestEnv}; - - use super::super::user::boilerplate::RootPartial; - - #[test] - fn parses_private_key_from_env() { - let env = TestEnv::new() - .set("PRIVATE_KEY", "8026408F4C15E5D664DA3F13778801D23D4E89B76E94C1B94B389544168B6CB894F84F8BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB"); - - let private_key = RootPartial::from_env(&env) - .expect("input is valid, should not fail") - .private_key - .get() - .expect("private key is provided, should not fail"); - - let (algorithm, payload) = private_key.to_bytes(); - assert_eq!(algorithm, "ed25519".parse().unwrap()); - assert_eq!(hex::encode(payload), "8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"); - } - - #[test] - fn fails_to_parse_private_key_from_env_with_invalid_value() { - let env = TestEnv::new().set("PRIVATE_KEY", "foo"); - let error = RootPartial::from_env(&env).expect_err("input is invalid, should fail"); - let expected = expect_test::expect![ - "failed to parse `iroha.private_key` field from `PRIVATE_KEY` env variable" - ]; - expected.assert_eq(&format!("{error:#}")); - } - - #[test] - fn deserialize_empty_input_works() { - let _layer: RootPartial = toml::from_str("").unwrap(); - } - - #[test] - fn deserialize_network_namespace_with_not_all_fields_works() { - let _layer: RootPartial = toml::toml! { - [network] - address = "127.0.0.1:8080" - } - .try_into() - .expect("should not fail when not all fields in `network` are presented at a time"); - } -} diff --git a/config/src/parameters/user/boilerplate.rs b/config/src/parameters/user/boilerplate.rs index 51492e53d0c..e69de29bb2d 100644 --- a/config/src/parameters/user/boilerplate.rs +++ b/config/src/parameters/user/boilerplate.rs @@ -1,777 +0,0 @@ -//! Code that should be generated by a procmacro in future. - -#![allow(missing_docs)] - -use std::{ - error::Error, - num::{NonZeroU32, NonZeroUsize}, - path::PathBuf, -}; - -use eyre::{eyre, WrapErr}; -use iroha_config_base::{ - Emitter, ErrorsCollection, ExtendsPaths, FromEnv, FromEnvDefaultFallback, FromEnvResult, - HumanBytes, HumanDuration, Merge, MissingFieldError, ParseEnvResult, ReadEnv, UnwrapPartial, - UnwrapPartialResult, UserField, -}; -use iroha_crypto::{PrivateKey, PublicKey}; -use iroha_data_model::{ - metadata::Limits as MetadataLimits, - prelude::{ChainId, PeerId}, - transaction::TransactionLimits, - LengthLimits, Level, -}; -use iroha_primitives::addr::SocketAddr; -use serde::{Deserialize, Serialize}; -use url::Url; - -use crate::{ - kura::InitMode as KuraInitMode, - logger::Format, - parameters::{ - defaults::{self, chain_wide::*, network::*, queue::*, torii::*}, - user::{ - ChainWide, DevTelemetry, Genesis, Kura, KuraDebug, Logger, Network, Queue, Root, - Snapshot, Sumeragi, SumeragiDebug, Telemetry, Torii, - }, - }, - snapshot::Mode as SnapshotMode, -}; - -#[derive(Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct RootPartial { - pub extends: Option, - pub chain_id: UserField, - pub public_key: UserField, - pub private_key: UserField, - pub genesis: GenesisPartial, - pub kura: KuraPartial, - pub sumeragi: SumeragiPartial, - pub network: NetworkPartial, - pub logger: LoggerPartial, - pub queue: QueuePartial, - pub snapshot: SnapshotPartial, - pub telemetry: TelemetryPartial, - pub dev_telemetry: DevTelemetryPartial, - pub torii: ToriiPartial, - pub chain_wide: ChainWidePartial, -} - -impl RootPartial { - /// Creates new empty user configuration - pub fn new() -> Self { - // TODO: generate this function with macro. For now, use default - Self::default() - } -} - -impl UnwrapPartial for RootPartial { - type Output = Root; - - fn unwrap_partial(self) -> UnwrapPartialResult { - let mut emitter = Emitter::new(); - - macro_rules! nested { - ($item:expr) => { - match UnwrapPartial::unwrap_partial($item) { - Ok(value) => Some(value), - Err(error) => { - emitter.emit_collection(error); - None - } - } - }; - } - - if self.chain_id.is_none() { - emitter.emit_missing_field("chain_id"); - } - if self.public_key.is_none() { - emitter.emit_missing_field("public_key"); - } - if self.private_key.is_none() { - emitter.emit_missing_field("private_key"); - } - - let genesis = nested!(self.genesis); - let kura = nested!(self.kura); - let sumeragi = nested!(self.sumeragi); - let network = nested!(self.network); - let logger = nested!(self.logger); - let queue = nested!(self.queue); - let snapshot = nested!(self.snapshot); - let telemetry = nested!(self.telemetry); - let dev_telemetry = nested!(self.dev_telemetry); - let torii = nested!(self.torii); - let chain_wide = nested!(self.chain_wide); - - emitter.finish()?; - - Ok(Root { - chain_id: self.chain_id.get().unwrap(), - public_key: self.public_key.get().unwrap(), - private_key: self.private_key.get().unwrap(), - genesis: genesis.unwrap(), - kura: kura.unwrap(), - sumeragi: sumeragi.unwrap(), - telemetry: telemetry.unwrap(), - dev_telemetry: dev_telemetry.unwrap(), - logger: logger.unwrap(), - queue: queue.unwrap(), - snapshot: snapshot.unwrap(), - torii: torii.unwrap(), - network: network.unwrap(), - chain_wide: chain_wide.unwrap(), - }) - } -} - -impl FromEnv for RootPartial { - fn from_env>(env: &R) -> FromEnvResult { - let mut emitter = Emitter::new(); - - let chain_id = env - .read_env("CHAIN_ID") - .map_err(|e| eyre!("{e}")) - .wrap_err("failed to read CHAIN_ID field (iroha.chain_id param)") - .map_or_else( - |err| { - emitter.emit(err); - None - }, - |maybe_value| maybe_value.map(ChainId::from), - ) - .into(); - let public_key = - ParseEnvResult::parse_simple(&mut emitter, env, "PUBLIC_KEY", "iroha.public_key") - .into(); - let private_key = - ParseEnvResult::parse_simple(&mut emitter, env, "PRIVATE_KEY", "iroha.private_key") - .into(); - - let genesis = emitter.try_from_env(env); - let kura = emitter.try_from_env(env); - let sumeragi = emitter.try_from_env(env); - let network = emitter.try_from_env(env); - let logger = emitter.try_from_env(env); - let queue = emitter.try_from_env(env); - let snapshot = emitter.try_from_env(env); - let telemetry = emitter.try_from_env(env); - let dev_telemetry = emitter.try_from_env(env); - let torii = emitter.try_from_env(env); - let chain_wide = emitter.try_from_env(env); - - emitter.finish()?; - - Ok(Self { - extends: None, - chain_id, - public_key, - private_key, - genesis: genesis.unwrap(), - kura: kura.unwrap(), - sumeragi: sumeragi.unwrap(), - network: network.unwrap(), - logger: logger.unwrap(), - queue: queue.unwrap(), - snapshot: snapshot.unwrap(), - telemetry: telemetry.unwrap(), - dev_telemetry: dev_telemetry.unwrap(), - torii: torii.unwrap(), - chain_wide: chain_wide.unwrap(), - }) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct GenesisPartial { - pub public_key: UserField, - pub private_key: UserField, - pub file: UserField, -} - -impl UnwrapPartial for GenesisPartial { - type Output = Genesis; - - fn unwrap_partial(self) -> UnwrapPartialResult { - let public_key = self - .public_key - .get() - .ok_or_else(|| MissingFieldError::new("genesis.public_key"))?; - - let private_key = self.private_key.get(); - let file = self.file.get(); - - Ok(Genesis { - public_key, - private_key, - file, - }) - } -} - -impl FromEnv for GenesisPartial { - fn from_env>(env: &R) -> FromEnvResult - where - Self: Sized, - { - let mut emitter = Emitter::new(); - - let public_key = ParseEnvResult::parse_simple( - &mut emitter, - env, - "GENESIS_PUBLIC_KEY", - "genesis.public_key", - ) - .into(); - let private_key = ParseEnvResult::parse_simple( - &mut emitter, - env, - "GENESIS_PRIVATE_KEY", - "genesis.private_key", - ) - .into(); - let file = - ParseEnvResult::parse_simple(&mut emitter, env, "GENESIS_FILE", "genesis.file").into(); - - emitter.finish()?; - - Ok(Self { - public_key, - private_key, - file, - }) - } -} - -/// `Kura` configuration. -#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct KuraPartial { - pub init_mode: UserField, - pub store_dir: UserField, - pub debug: KuraDebugPartial, -} - -impl UnwrapPartial for KuraPartial { - type Output = Kura; - - fn unwrap_partial(self) -> Result> { - let mut emitter = Emitter::new(); - - let init_mode = self.init_mode.unwrap_or_default(); - - let store_dir = self - .store_dir - .get() - .unwrap_or_else(|| PathBuf::from(defaults::kura::DEFAULT_STORE_DIR)); - - let debug = UnwrapPartial::unwrap_partial(self.debug).map_or_else( - |err| { - emitter.emit_collection(err); - None - }, - Some, - ); - - emitter.finish()?; - - Ok(Kura { - init_mode, - store_dir, - debug: debug.unwrap(), - }) - } -} - -#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct KuraDebugPartial { - output_new_blocks: UserField, -} - -impl UnwrapPartial for KuraDebugPartial { - type Output = KuraDebug; - - fn unwrap_partial(self) -> Result> { - Ok(KuraDebug { - output_new_blocks: self.output_new_blocks.unwrap_or(false), - }) - } -} - -impl FromEnv for KuraPartial { - fn from_env>(env: &R) -> FromEnvResult - where - Self: Sized, - { - let mut emitter = Emitter::new(); - - let init_mode = - ParseEnvResult::parse_simple(&mut emitter, env, "KURA_INIT_MODE", "kura.init_mode") - .into(); - let store_dir = - ParseEnvResult::parse_simple(&mut emitter, env, "KURA_STORE_DIR", "kura.store_dir") - .into(); - let debug_output_new_blocks = ParseEnvResult::parse_simple( - &mut emitter, - env, - "KURA_DEBUG_OUTPUT_NEW_BLOCKS", - "kura.debug.output_new_blocks", - ) - .into(); - - emitter.finish()?; - - Ok(Self { - init_mode, - store_dir, - debug: KuraDebugPartial { - output_new_blocks: debug_output_new_blocks, - }, - }) - } -} - -#[derive(Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct SumeragiPartial { - pub trusted_peers: UserField>, - pub debug: SumeragiDebugPartial, -} - -impl UnwrapPartial for SumeragiPartial { - type Output = Sumeragi; - - fn unwrap_partial(self) -> UnwrapPartialResult { - let mut emitter = Emitter::new(); - - let debug = self.debug.unwrap_partial().map_or_else( - |err| { - emitter.emit_collection(err); - None - }, - Some, - ); - - emitter.finish()?; - - Ok(Sumeragi { - trusted_peers: self.trusted_peers.get(), - debug: debug.unwrap(), - }) - } -} - -#[derive(Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct SumeragiDebugPartial { - pub force_soft_fork: UserField, -} - -impl UnwrapPartial for SumeragiDebugPartial { - type Output = SumeragiDebug; - - fn unwrap_partial(self) -> UnwrapPartialResult { - Ok(SumeragiDebug { - force_soft_fork: self.force_soft_fork.unwrap_or(false), - }) - } -} - -impl FromEnv for SumeragiPartial { - fn from_env>(env: &R) -> FromEnvResult - where - Self: Sized, - { - let mut emitter = Emitter::new(); - - let trusted_peers = ParseEnvResult::parse_json( - &mut emitter, - env, - "SUMERAGI_TRUSTED_PEERS", - "sumeragi.trusted_peers", - ) - .into(); - let debug = emitter.try_from_env(env); - - emitter.finish()?; - - Ok(Self { - trusted_peers, - debug: debug.unwrap(), - }) - } -} - -impl FromEnvDefaultFallback for SumeragiDebugPartial {} - -#[derive(Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct NetworkPartial { - pub address: UserField, - pub block_gossip_max_size: UserField, - pub block_gossip_period: UserField, - pub transaction_gossip_max_size: UserField, - pub transaction_gossip_period: UserField, - pub idle_timeout: UserField, -} - -impl UnwrapPartial for NetworkPartial { - type Output = Network; - - fn unwrap_partial(self) -> UnwrapPartialResult { - if self.address.is_none() { - return Err(MissingFieldError::new("network.address").into()); - } - - Ok(Network { - address: self.address.get().unwrap(), - block_gossip_period: self - .block_gossip_period - .map(HumanDuration::get) - .unwrap_or(DEFAULT_BLOCK_GOSSIP_PERIOD), - transaction_gossip_period: self - .transaction_gossip_period - .map(HumanDuration::get) - .unwrap_or(DEFAULT_TRANSACTION_GOSSIP_PERIOD), - transaction_gossip_max_size: self - .transaction_gossip_max_size - .get() - .unwrap_or(DEFAULT_MAX_TRANSACTIONS_PER_GOSSIP), - block_gossip_max_size: self - .block_gossip_max_size - .get() - .unwrap_or(DEFAULT_MAX_BLOCKS_PER_GOSSIP), - idle_timeout: self - .idle_timeout - .map(HumanDuration::get) - .unwrap_or(DEFAULT_IDLE_TIMEOUT), - }) - } -} - -impl FromEnv for NetworkPartial { - fn from_env>(env: &R) -> FromEnvResult - where - Self: Sized, - { - let mut emitter = Emitter::new(); - - // TODO: also parse `NETWORK_ADDRESS`? - let address = - ParseEnvResult::parse_simple(&mut emitter, env, "P2P_ADDRESS", "network.address") - .into(); - - emitter.finish()?; - - Ok(Self { - address, - ..Self::default() - }) - } -} - -#[derive(Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct QueuePartial { - /// The upper limit of the number of transactions waiting in the queue. - pub capacity: UserField, - /// The upper limit of the number of transactions waiting in the queue for single user. - /// Use this option to apply throttling. - pub capacity_per_user: UserField, - /// The transaction will be dropped after this time if it is still in the queue. - pub transaction_time_to_live: UserField, - /// The threshold to determine if a transaction has been tampered to have a future timestamp. - pub future_threshold: UserField, -} - -impl UnwrapPartial for QueuePartial { - type Output = Queue; - - fn unwrap_partial(self) -> UnwrapPartialResult { - Ok(Queue { - capacity: self.capacity.unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), - capacity_per_user: self - .capacity_per_user - .unwrap_or(DEFAULT_MAX_TRANSACTIONS_IN_QUEUE), - transaction_time_to_live: self - .transaction_time_to_live - .map_or(DEFAULT_TRANSACTION_TIME_TO_LIVE, HumanDuration::get), - future_threshold: self - .future_threshold - .map_or(DEFAULT_FUTURE_THRESHOLD, HumanDuration::get), - }) - } -} - -impl FromEnvDefaultFallback for QueuePartial {} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct LoggerPartial { - pub level: UserField, - pub format: UserField, -} - -impl UnwrapPartial for LoggerPartial { - type Output = Logger; - - fn unwrap_partial(self) -> UnwrapPartialResult { - Ok(Logger { - level: self.level.unwrap_or_default(), - format: self.format.unwrap_or_default(), - }) - } -} - -impl FromEnv for LoggerPartial { - fn from_env>(env: &R) -> FromEnvResult - where - Self: Sized, - { - let mut emitter = Emitter::new(); - - let level = - ParseEnvResult::parse_simple(&mut emitter, env, "LOG_LEVEL", "logger.level").into(); - let format = - ParseEnvResult::parse_simple(&mut emitter, env, "LOG_FORMAT", "logger.format").into(); - - emitter.finish()?; - - #[allow(clippy::needless_update)] // triggers if tokio console addr is feature-gated - Ok(Self { - level, - format, - ..Self::default() - }) - } -} - -#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct TelemetryPartial { - pub name: UserField, - pub url: UserField, - pub min_retry_period: UserField, - pub max_retry_delay_exponent: UserField, -} - -#[derive(Clone, Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct DevTelemetryPartial { - pub out_file: UserField, -} - -impl UnwrapPartial for DevTelemetryPartial { - type Output = DevTelemetry; - - fn unwrap_partial(self) -> UnwrapPartialResult { - Ok(DevTelemetry { - out_file: self.out_file.get(), - }) - } -} - -impl UnwrapPartial for TelemetryPartial { - type Output = Telemetry; - - fn unwrap_partial(self) -> UnwrapPartialResult { - let Self { - name, - url, - max_retry_delay_exponent, - min_retry_period, - } = self; - - Ok(Telemetry { - name: name.get(), - url: url.get(), - max_retry_delay_exponent: max_retry_delay_exponent.get(), - min_retry_period: min_retry_period.get().map(HumanDuration::get), - }) - } -} - -impl FromEnvDefaultFallback for TelemetryPartial {} - -impl FromEnvDefaultFallback for DevTelemetryPartial {} - -#[derive(Debug, Clone, Deserialize, Serialize, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct SnapshotPartial { - pub mode: UserField, - pub create_every: UserField, - pub store_dir: UserField, -} - -impl UnwrapPartial for SnapshotPartial { - type Output = Snapshot; - - fn unwrap_partial(self) -> UnwrapPartialResult { - Ok(Snapshot { - mode: self.mode.unwrap_or_default(), - create_every: self - .create_every - .get() - .map_or(defaults::snapshot::DEFAULT_CREATE_EVERY, HumanDuration::get), - store_dir: self - .store_dir - .get() - .unwrap_or_else(|| PathBuf::from(defaults::snapshot::DEFAULT_STORE_DIR)), - }) - } -} - -impl FromEnv for SnapshotPartial { - fn from_env>(env: &R) -> FromEnvResult - where - Self: Sized, - { - let mut emitter = Emitter::new(); - - let mode = - ParseEnvResult::parse_simple(&mut emitter, env, "SNAPSHOT_MODE", "snapshot.mode") - .into(); - let store_dir = ParseEnvResult::parse_simple( - &mut emitter, - env, - "SNAPSHOT_STORE_DIR", - "snapshot.store_dir", - ) - .into(); - - emitter.finish()?; - - Ok(Self { - mode, - store_dir, - ..Self::default() - }) - } -} - -#[derive(Deserialize, Serialize, Debug, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct ChainWidePartial { - pub max_transactions_in_block: UserField, - pub block_time: UserField, - pub commit_time: UserField, - pub transaction_limits: UserField, - pub domain_metadata_limits: UserField, - pub asset_definition_metadata_limits: UserField, - pub account_metadata_limits: UserField, - pub asset_metadata_limits: UserField, - pub trigger_metadata_limits: UserField, - pub ident_length_limits: UserField, - pub executor_fuel_limit: UserField, - pub executor_max_memory: UserField>, - pub wasm_fuel_limit: UserField, - pub wasm_max_memory: UserField>, -} - -impl UnwrapPartial for ChainWidePartial { - type Output = ChainWide; - - fn unwrap_partial(self) -> UnwrapPartialResult { - Ok(ChainWide { - max_transactions_in_block: self.max_transactions_in_block.unwrap_or(DEFAULT_MAX_TXS), - block_time: self - .block_time - .map_or(DEFAULT_BLOCK_TIME, HumanDuration::get), - commit_time: self - .commit_time - .map_or(DEFAULT_COMMIT_TIME, HumanDuration::get), - transaction_limits: self - .transaction_limits - .unwrap_or(DEFAULT_TRANSACTION_LIMITS), - domain_metadata_limits: self - .domain_metadata_limits - .unwrap_or(DEFAULT_METADATA_LIMITS), - asset_definition_metadata_limits: self - .asset_definition_metadata_limits - .unwrap_or(DEFAULT_METADATA_LIMITS), - account_metadata_limits: self - .account_metadata_limits - .unwrap_or(DEFAULT_METADATA_LIMITS), - asset_metadata_limits: self - .asset_metadata_limits - .unwrap_or(DEFAULT_METADATA_LIMITS), - trigger_metadata_limits: self - .trigger_metadata_limits - .unwrap_or(DEFAULT_METADATA_LIMITS), - ident_length_limits: self - .ident_length_limits - .unwrap_or(DEFAULT_IDENT_LENGTH_LIMITS), - executor_fuel_limit: self.executor_fuel_limit.unwrap_or(DEFAULT_WASM_FUEL_LIMIT), - executor_max_memory: self - .executor_max_memory - .unwrap_or(HumanBytes(DEFAULT_WASM_MAX_MEMORY_BYTES)), - wasm_fuel_limit: self.wasm_fuel_limit.unwrap_or(DEFAULT_WASM_FUEL_LIMIT), - wasm_max_memory: self - .wasm_max_memory - .unwrap_or(HumanBytes(DEFAULT_WASM_MAX_MEMORY_BYTES)), - }) - } -} - -impl FromEnvDefaultFallback for ChainWidePartial {} - -#[derive(Debug, Clone, Deserialize, Serialize, Default, Merge)] -#[serde(deny_unknown_fields, default)] -pub struct ToriiPartial { - pub address: UserField, - pub max_content_len: UserField>, - pub query_idle_time: UserField, -} - -impl UnwrapPartial for ToriiPartial { - type Output = Torii; - - fn unwrap_partial(self) -> UnwrapPartialResult { - let mut emitter = Emitter::new(); - - if self.address.is_none() { - emitter.emit_missing_field("torii.address"); - } - - let max_content_len = self - .max_content_len - .get() - .unwrap_or(HumanBytes(DEFAULT_MAX_CONTENT_LENGTH)); - - let query_idle_time = self - .query_idle_time - .map(HumanDuration::get) - .unwrap_or(DEFAULT_QUERY_IDLE_TIME); - - emitter.finish()?; - - Ok(Torii { - address: self.address.get().unwrap(), - max_content_len, - query_idle_time, - }) - } -} - -impl FromEnv for ToriiPartial { - fn from_env>(env: &R) -> FromEnvResult - where - Self: Sized, - { - let mut emitter = Emitter::new(); - - let address = - ParseEnvResult::parse_simple(&mut emitter, env, "API_ADDRESS", "torii.address").into(); - - emitter.finish()?; - - Ok(Self { - address, - ..Self::default() - }) - } -} diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index e210b3946a9..97c4baa31b3 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -1,4 +1,4 @@ -#![allow(clippy::needless_raw_string_hashes)] // triggered by `expect_test` snapshots +#![allow(clippy::needless_raw_string_hashes)] // triggered by `expect!` snapshots use std::{ collections::{HashMap, HashSet}, @@ -6,12 +6,12 @@ use std::{ path::{Path, PathBuf}, }; -use eyre::Result; -use iroha_config::parameters::{ - actual::{Genesis, Root}, - user::{CliContext, RootPartial}, -}; -use iroha_config_base::{FromEnv, TestEnv, UnwrapPartial as _}; +use assertables::{assert_contains, assert_contains_as_result}; +use error_stack::ResultExt; +use expect_test::expect; +use iroha_config::parameters::{actual::Root as Config, user::Root as UserConfig}; +use iroha_config_base::{env::MockEnv, read::ConfigReader}; +use thiserror::Error; fn fixtures_dir() -> PathBuf { // CWD is the crate's root @@ -34,24 +34,39 @@ fn parse_env(raw: impl AsRef) -> HashMap { .collect() } -fn test_env_from_file(p: impl AsRef) -> TestEnv { +fn test_env_from_file(p: impl AsRef) -> MockEnv { let contents = fs::read_to_string(p).expect("the path should be valid"); let map = parse_env(contents); - TestEnv::with_map(map) + MockEnv::with_map(map) +} + +#[derive(Error, Debug)] +#[error("failed to load config from fixtures")] +struct FixtureConfigLoadError; + +fn load_config_from_fixtures( + path: impl AsRef, +) -> error_stack::Result { + let config = ConfigReader::new() + .read_toml_with_extends(fixtures_dir().join(path)) + .change_context(FixtureConfigLoadError)? + .read_and_complete::() + .change_context(FixtureConfigLoadError)? + .parse() + .change_context(FixtureConfigLoadError)?; + + Ok(config) } /// This test not only asserts that the minimal set of fields is enough; /// it also gives an insight into every single default value #[test] #[allow(clippy::too_many_lines)] -fn minimal_config_snapshot() -> Result<()> { - let config = RootPartial::from_toml(fixtures_dir().join("minimal_with_trusted_peers.toml"))? - .unwrap_partial()? - .parse(CliContext { - submit_genesis: false, - })?; +fn minimal_config_snapshot() { + let config = load_config_from_fixtures("minimal_with_trusted_peers.toml") + .expect("config should be valid"); - let expected = expect_test::expect![[r#" + expect![[r#" Root { common: Common { chain_id: ChainId( @@ -75,7 +90,13 @@ fn minimal_config_snapshot() -> Result<()> { }, }, network: Network { - address: 127.0.0.1:1337, + address: WithOrigin { + value: 127.0.0.1:1337, + origin: File { + id: ParameterId(network.address), + path: "tests/fixtures/base.toml", + }, + }, idle_timeout: 60s, }, genesis: Partial { @@ -86,27 +107,54 @@ fn minimal_config_snapshot() -> Result<()> { ), }, torii: Torii { - address: 127.0.0.1:8080, + address: WithOrigin { + value: 127.0.0.1:8080, + origin: File { + id: ParameterId(torii.address), + path: "tests/fixtures/base.toml", + }, + }, max_content_len_bytes: 16777216, }, kura: Kura { init_mode: Strict, - store_dir: "./storage", + store_dir: WithOrigin { + value: "./storage", + origin: Default { + id: ParameterId(kura.store_dir), + }, + }, debug_output_new_blocks: false, }, sumeragi: Sumeragi { - trusted_peers: UniqueVec( - [ - PeerId { - address: 127.0.0.1:1338, + trusted_peers: WithOrigin { + value: TrustedPeers { + myself: PeerId { + address: 127.0.0.1:1337, public_key: PublicKey( ed25519( "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB", ), ), }, - ], - ), + others: UniqueVec( + [ + PeerId { + address: 127.0.0.1:1338, + public_key: PublicKey( + ed25519( + "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB", + ), + ), + }, + ], + ), + }, + origin: File { + id: ParameterId(sumeragi.trusted_peers), + path: "tests/fixtures/base_trusted_peers.toml", + }, + }, debug_force_soft_fork: false, }, block_sync: BlockSync { @@ -132,8 +180,15 @@ fn minimal_config_snapshot() -> Result<()> { }, snapshot: Snapshot { mode: ReadWrite, - create_every: 60s, - store_dir: "./storage/snapshot", + create_every: HumanDuration( + 60s, + ), + store_dir: WithOrigin { + value: "./storage/snapshot", + origin: Default { + id: ParameterId(snapshot.store_dir), + }, + }, }, telemetry: None, dev_telemetry: DevTelemetry { @@ -180,370 +235,90 @@ fn minimal_config_snapshot() -> Result<()> { max_memory_bytes: 524288000, }, }, - }"#]]; - expected.assert_eq(&format!("{config:#?}")); - - Ok(()) + }"#]].assert_eq(&format!("{config:#?}")); } #[test] -fn config_with_genesis() -> Result<()> { - let _config = RootPartial::from_toml(fixtures_dir().join("minimal_alone_with_genesis.toml"))? - .unwrap_partial()? - .parse(CliContext { - submit_genesis: true, - })?; - Ok(()) +fn config_with_genesis() { + let _config = + load_config_from_fixtures("minimal_alone_with_genesis.toml").expect("should be valid"); } #[test] -fn minimal_with_genesis_but_no_cli_arg_fails() -> Result<()> { - let error = RootPartial::from_toml(fixtures_dir().join("minimal_alone_with_genesis.toml"))? - .unwrap_partial()? - .parse(CliContext { - submit_genesis: false, - }) - .expect_err("should fail since `--submit-genesis=false`"); - - let expected = expect_test::expect![[r#" - `genesis.file` and `genesis.private_key` are presented, but `--submit-genesis` was not set - The network consists from this one peer only (no `sumeragi.trusted_peers` provided). Since `--submit-genesis` is not set, there is no way to receive the genesis block. Either provide the genesis by setting `--submit-genesis` argument, `genesis.private_key`, and `genesis.file` configuration parameters, or increase the number of trusted peers in the network using `sumeragi.trusted_peers` configuration parameter."#]]; - expected.assert_eq(&format!("{error:#}")); - - Ok(()) -} - -#[test] -fn minimal_without_genesis_but_with_submit_fails() -> Result<()> { - let error = RootPartial::from_toml(fixtures_dir().join("minimal_with_trusted_peers.toml"))? - .unwrap_partial()? - .parse(CliContext { - submit_genesis: true, - }) - .expect_err( - "should fail since there is no genesis in the config, but `--submit-genesis=true`", - ); - - let expected = expect_test::expect!["`--submit-genesis` was set, but `genesis.file` and `genesis.private_key` are not presented"]; - expected.assert_eq(&format!("{error:#}")); - - Ok(()) -} - -#[test] -fn self_is_presented_in_trusted_peers() -> Result<()> { - let config = RootPartial::from_toml(fixtures_dir().join("minimal_alone_with_genesis.toml"))? - .unwrap_partial()? - .parse(CliContext { - submit_genesis: true, - })?; +fn self_is_presented_in_trusted_peers() { + let config = + load_config_from_fixtures("minimal_alone_with_genesis.toml").expect("valid config"); assert!(config .sumeragi .trusted_peers + .value() + .clone() + .into_non_empty_vec() .contains(&config.common.peer_id())); - - Ok(()) } #[test] -fn missing_fields() -> Result<()> { - let error = RootPartial::from_toml(fixtures_dir().join("bad.missing_fields.toml"))? - .unwrap_partial() - .expect_err("should fail with missing fields"); +fn missing_fields() { + let error = load_config_from_fixtures("bad.missing_fields.toml") + .expect_err("should fail without missing fields"); - let expected = expect_test::expect![[r#" - missing field: `chain_id` - missing field: `public_key` - missing field: `private_key` - missing field: `genesis.public_key` - missing field: `network.address` - missing field: `torii.address`"#]]; - expected.assert_eq(&format!("{error:#}")); - - Ok(()) + assert_contains!(format!("{error:?}"), "missing parameter: `chain_id`"); + assert_contains!(format!("{error:?}"), "missing parameter: `public_key`"); + assert_contains!(format!("{error:?}"), "missing parameter: `network.address`"); } #[test] fn extra_fields() { - let error = RootPartial::from_toml(fixtures_dir().join("extra_fields.toml")) - .expect_err("should fail with extra fields"); + let error = load_config_from_fixtures("bad.extra_fields.toml") + .expect_err("should fail with extra field"); - let expected = expect_test::expect!["cannot open file at location `tests/fixtures/extra_fields.toml`: No such file or directory (os error 2)"]; - expected.assert_eq(&format!("{error:#}")); + assert_contains!(format!("{error:?}"), "Found unrecognised parameters"); + assert_contains!(format!("{error:?}"), "unknown parameter: `bar`"); + assert_contains!(format!("{error:?}"), "unknown parameter: `foo`"); } #[test] -fn inconsistent_genesis_config() -> Result<()> { - let error = RootPartial::from_toml(fixtures_dir().join("inconsistent_genesis.toml"))? - .unwrap_partial() - .expect("all fields are present") - .parse(CliContext { - submit_genesis: false, - }) +fn inconsistent_genesis_config() { + let error = load_config_from_fixtures("inconsistent_genesis.toml") .expect_err("should fail with bad genesis config"); - let expected = expect_test::expect![[r#" - `genesis.file` and `genesis.private_key` should be set together - The network consists from this one peer only (no `sumeragi.trusted_peers` provided). Since `--submit-genesis` is not set, there is no way to receive the genesis block. Either provide the genesis by setting `--submit-genesis` argument, `genesis.private_key`, and `genesis.file` configuration parameters, or increase the number of trusted peers in the network using `sumeragi.trusted_peers` configuration parameter."#]]; - expected.assert_eq(&format!("{error:#}")); - - Ok(()) + assert_contains!( + format!("{error:?}"), + "`genesis.private_key` is set, but `genesis.file` is not" + ); } /// Aims the purpose of checking that every single provided env variable is consumed and parsed /// into a valid config. #[test] -#[allow(clippy::too_many_lines)] -fn full_envs_set_is_consumed() -> Result<()> { +fn full_envs_set_is_consumed() { let env = test_env_from_file(fixtures_dir().join("full.env")); - let layer = RootPartial::from_env(&env)?; + ConfigReader::new() + .with_env(env.clone()) + .read_and_complete::() + .expect("should be fine"); assert_eq!(env.unvisited(), HashSet::new()); - - let expected = expect_test::expect![[r#" - RootPartial { - extends: None, - chain_id: Some( - ChainId( - "0-0", - ), - ), - public_key: Some( - PublicKey( - ed25519( - "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB", - ), - ), - ), - private_key: Some( - "[REDACTED PrivateKey]", - ), - genesis: GenesisPartial { - public_key: Some( - PublicKey( - ed25519( - "ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB", - ), - ), - ), - private_key: Some( - "[REDACTED PrivateKey]", - ), - file: None, - }, - kura: KuraPartial { - init_mode: Some( - Strict, - ), - store_dir: Some( - "/store/path/from/env", - ), - debug: KuraDebugPartial { - output_new_blocks: Some( - false, - ), - }, - }, - sumeragi: SumeragiPartial { - trusted_peers: Some( - [ - PeerId { - address: SocketAddrHost { - host: "iroha2", - port: 1339, - }, - public_key: PublicKey( - ed25519( - "ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4", - ), - ), - }, - ], - ), - debug: SumeragiDebugPartial { - force_soft_fork: None, - }, - }, - network: NetworkPartial { - address: Some( - 127.0.0.1:5432, - ), - block_gossip_max_size: None, - block_gossip_period: None, - transaction_gossip_max_size: None, - transaction_gossip_period: None, - idle_timeout: None, - }, - logger: LoggerPartial { - level: Some( - DEBUG, - ), - format: Some( - Pretty, - ), - }, - queue: QueuePartial { - capacity: None, - capacity_per_user: None, - transaction_time_to_live: None, - future_threshold: None, - }, - snapshot: SnapshotPartial { - mode: Some( - ReadWrite, - ), - create_every: None, - store_dir: Some( - "/snapshot/path/from/env", - ), - }, - telemetry: TelemetryPartial { - name: None, - url: None, - min_retry_period: None, - max_retry_delay_exponent: None, - }, - dev_telemetry: DevTelemetryPartial { - out_file: None, - }, - torii: ToriiPartial { - address: Some( - 127.0.0.1:8080, - ), - max_content_len: None, - query_idle_time: None, - }, - chain_wide: ChainWidePartial { - max_transactions_in_block: None, - block_time: None, - commit_time: None, - transaction_limits: None, - domain_metadata_limits: None, - asset_definition_metadata_limits: None, - account_metadata_limits: None, - asset_metadata_limits: None, - trigger_metadata_limits: None, - ident_length_limits: None, - executor_fuel_limit: None, - executor_max_memory: None, - wasm_fuel_limit: None, - wasm_max_memory: None, - }, - }"#]]; - expected.assert_eq(&format!("{layer:#?}")); - - Ok(()) + assert_eq!(env.unknown(), HashSet::new()); } #[test] -fn multiple_env_parsing_errors() { - let env = test_env_from_file(fixtures_dir().join("bad.multiple_bad_envs.env")); - - let error = RootPartial::from_env(&env).expect_err("the input from env is invalid"); - - let expected = expect_test::expect![[r#" - failed to parse `iroha.private_key` field from `PRIVATE_KEY` env variable - failed to parse `genesis.private_key` field from `GENESIS_PRIVATE_KEY` env variable - failed to parse `kura.debug.output_new_blocks` field from `KURA_DEBUG_OUTPUT_NEW_BLOCKS` env variable - failed to parse `logger.format` field from `LOG_FORMAT` env variable - failed to parse `torii.address` field from `API_ADDRESS` env variable"#]]; - expected.assert_eq(&format!("{error:#}")); -} - -#[test] -fn config_from_file_and_env() -> Result<()> { +fn config_from_file_and_env() { let env = test_env_from_file(fixtures_dir().join("minimal_file_and_env.env")); - let _config = RootPartial::from_toml(fixtures_dir().join("minimal_file_and_env.toml"))? - .merge(RootPartial::from_env(&env)?) - .unwrap_partial()? - .parse(CliContext { - submit_genesis: false, - })?; - - Ok(()) -} - -#[test] -fn fails_if_torii_address_and_p2p_address_are_equal() -> Result<()> { - let error = RootPartial::from_toml(fixtures_dir().join("bad.torii_addr_eq_p2p_addr.toml"))? - .unwrap_partial() - .expect("should not fail, all fields are present") - .parse(CliContext { - submit_genesis: false, - }) - .expect_err("should fail because of bad input"); - - let expected = - expect_test::expect!["`iroha.p2p_address` and `torii.address` should not be the same"]; - expected.assert_eq(&format!("{error:#}")); - - Ok(()) -} - -#[test] -fn fails_if_extends_leads_to_nowhere() { - let error = RootPartial::from_toml(fixtures_dir().join("bad.extends_nowhere.toml")) - .expect_err("should fail with bad input"); - - let expected = expect_test::expect!["cannot extend from `tests/fixtures/nowhere.toml`: cannot open file at location `tests/fixtures/nowhere.toml`: No such file or directory (os error 2)"]; - expected.assert_eq(&format!("{error:#}")); -} - -#[test] -fn multiple_extends_works() -> Result<()> { - // we are looking into `logger` in particular - let layer = RootPartial::from_toml(fixtures_dir().join("multiple_extends.toml"))?.logger; - - let expected = expect_test::expect![[r#" - LoggerPartial { - level: Some( - ERROR, - ), - format: Some( - Compact, - ), - }"#]]; - expected.assert_eq(&format!("{layer:#?}")); - - Ok(()) + ConfigReader::new() + .with_env(env) + .read_toml_with_extends(fixtures_dir().join("minimal_file_and_env.toml")) + .expect("files are fine") + .read_and_complete::() + .expect("should be fine") + .parse() + .expect("should be fine, again"); } #[test] fn full_config_parses_fine() { - let _cfg = Root::load( - Some(fixtures_dir().join("full.toml")), - CliContext { - submit_genesis: true, - }, - ) - .expect("should be fine"); -} - -#[test] -fn absolute_paths_are_preserved() { - let cfg = Root::load( - Some(fixtures_dir().join("absolute_paths.toml")), - CliContext { - submit_genesis: true, - }, - ) - .expect("should be fine"); - - assert_eq!(cfg.kura.store_dir, PathBuf::from("/kura/store")); - assert_eq!(cfg.snapshot.store_dir, PathBuf::from("/snapshot/store")); - assert_eq!( - cfg.dev_telemetry.out_file.unwrap(), - PathBuf::from("/telemetry/file.json") - ); - if let Genesis::Full { - file: genesis_file, .. - } = cfg.genesis - { - assert_eq!(genesis_file, PathBuf::from("/oh/my/genesis.json")); - } else { - unreachable!() - }; + let _cfg = load_config_from_fixtures("full.toml").expect("should be fine"); } diff --git a/config/tests/fixtures/bad.extends_nowhere.toml b/config/tests/fixtures/bad.extends_nowhere.toml deleted file mode 100644 index 30129b39359..00000000000 --- a/config/tests/fixtures/bad.extends_nowhere.toml +++ /dev/null @@ -1 +0,0 @@ -extends = "nowhere.toml" \ No newline at end of file diff --git a/config/tests/fixtures/full.env b/config/tests/fixtures/full.env index ea6e3bf5956..a1559eddaa7 100644 --- a/config/tests/fixtures/full.env +++ b/config/tests/fixtures/full.env @@ -4,6 +4,7 @@ PRIVATE_KEY=8026408F4C15E5D664DA3F13778801D23D4E89B76E94C1B94B389544168B6CB894F8 P2P_ADDRESS=127.0.0.1:5432 GENESIS_PUBLIC_KEY=ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB GENESIS_PRIVATE_KEY=8026408F4C15E5D664DA3F13778801D23D4E89B76E94C1B94B389544168B6CB894F84F8BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB +GENESIS_FILE=./genesis.json API_ADDRESS=127.0.0.1:8080 KURA_INIT_MODE=strict KURA_STORE_DIR=/store/path/from/env diff --git a/config/tests/fixtures/full.toml b/config/tests/fixtures/full.toml index 663ab418099..5c2e0eddd74 100644 --- a/config/tests/fixtures/full.toml +++ b/config/tests/fixtures/full.toml @@ -18,7 +18,7 @@ transaction_gossip_max_size = 500 [torii] address = "localhost:5000" -max_content_len = 16 +max_content_length = 16 query_idle_time = 30_000 [kura] diff --git a/config/tests/fixtures/multiple_extends.1.toml b/config/tests/fixtures/multiple_extends.1.toml deleted file mode 100644 index 46b1262777b..00000000000 --- a/config/tests/fixtures/multiple_extends.1.toml +++ /dev/null @@ -1,2 +0,0 @@ -[logger] -format = "pretty" \ No newline at end of file diff --git a/config/tests/fixtures/multiple_extends.2.toml b/config/tests/fixtures/multiple_extends.2.toml deleted file mode 100644 index 47e9616ccfd..00000000000 --- a/config/tests/fixtures/multiple_extends.2.toml +++ /dev/null @@ -1,5 +0,0 @@ -# sets level -extends = "multiple_extends.2a.toml" - -[logger] -format = "compact" \ No newline at end of file diff --git a/config/tests/fixtures/multiple_extends.2a.toml b/config/tests/fixtures/multiple_extends.2a.toml deleted file mode 100644 index c7b048bc674..00000000000 --- a/config/tests/fixtures/multiple_extends.2a.toml +++ /dev/null @@ -1,2 +0,0 @@ -[logger] -level = "DEBUG" \ No newline at end of file diff --git a/config/tests/fixtures/multiple_extends.toml b/config/tests/fixtures/multiple_extends.toml deleted file mode 100644 index 83b87043034..00000000000 --- a/config/tests/fixtures/multiple_extends.toml +++ /dev/null @@ -1,6 +0,0 @@ -# 1 - sets format, 2 - sets format and level -extends = ["multiple_extends.1.toml", "multiple_extends.2.toml"] - -[logger] -# final value -level = "ERROR" \ No newline at end of file diff --git a/configs/peer.template.toml b/configs/peer.template.toml index cce389bf000..d2f9c19f520 100644 --- a/configs/peer.template.toml +++ b/configs/peer.template.toml @@ -28,7 +28,7 @@ [torii] # address = -# max_content_len = "16mb" +# max_content_length = "16mb" # query_idle_time = "30s" [kura] diff --git a/core/benches/kura.rs b/core/benches/kura.rs index a0ba9fb1139..5483677c2ee 100644 --- a/core/benches/kura.rs +++ b/core/benches/kura.rs @@ -4,7 +4,7 @@ use std::str::FromStr as _; use byte_unit::{Byte, UnitType}; use criterion::{criterion_group, criterion_main, Criterion}; -use iroha_config::parameters::actual::Kura as Config; +use iroha_config::{base::WithOrigin, parameters::actual::Kura as Config}; use iroha_core::{ block::*, kura::{BlockStore, LockStatus}, @@ -40,7 +40,7 @@ async fn measure_block_size_for_n_executors(n_executors: u32) { let cfg = Config { init_mode: iroha_config::kura::InitMode::Strict, debug_output_new_blocks: false, - store_dir: dir.path().to_path_buf(), + store_dir: WithOrigin::inline(dir.path().to_path_buf()), }; let kura = iroha_core::kura::Kura::new(&cfg).unwrap(); let _thread_handle = iroha_core::kura::Kura::start(kura.clone()); diff --git a/core/src/block.rs b/core/src/block.rs index 941cf2b0a28..c6d217e528a 100644 --- a/core/src/block.rs +++ b/core/src/block.rs @@ -6,7 +6,7 @@ //! [`Block`]s are organised into a linear sequence over time (also known as the block chain). use std::error::Error as _; -use iroha_config::parameters::defaults::chain_wide::DEFAULT_CONSENSUS_ESTIMATION; +use iroha_config::parameters::defaults::chain_wide::CONSENSUS_ESTIMATION as DEFAULT_CONSENSUS_ESTIMATION; use iroha_crypto::{HashOf, KeyPair, MerkleTree, SignatureOf, SignaturesOf}; use iroha_data_model::{ block::*, diff --git a/core/src/kiso.rs b/core/src/kiso.rs index c99add91be0..67837c12816 100644 --- a/core/src/kiso.rs +++ b/core/src/kiso.rs @@ -151,23 +151,21 @@ mod tests { use std::time::Duration; use iroha_config::{ + base::{read::ConfigReader, toml::TomlSource}, client_api::{ConfigDTO, Logger as LoggerDTO}, - parameters::actual::Root, + parameters::{actual::Root, user::Root as UserConfig}, }; use super::*; fn test_config() -> Root { - use iroha_config::parameters::user::CliContext; - - Root::load( - // FIXME Specifying path here might break! - Some("../config/iroha_test_config.toml"), - CliContext { - submit_genesis: true, - }, - ) - .expect("test config should be valid, it is probably a bug") + // if it fails, it is probably a bug + ConfigReader::new() + .with_toml_source(TomlSource::from_file("../config/iroha_test_config.toml").unwrap()) + .read_and_complete::() + .unwrap() + .parse() + .unwrap() } #[tokio::test] diff --git a/core/src/kura.rs b/core/src/kura.rs index ef36bbdf19e..19b8baf3d75 100644 --- a/core/src/kura.rs +++ b/core/src/kura.rs @@ -50,12 +50,13 @@ impl Kura { /// to access the block store indicated by the provided /// path. pub fn new(config: &Config) -> Result> { - let mut block_store = BlockStore::new(&config.store_dir, LockStatus::Unlocked); + let store_dir = config.store_dir.resolve_relative_path(); + let mut block_store = BlockStore::new(&store_dir, LockStatus::Unlocked); block_store.create_files_if_they_do_not_exist()?; let block_plain_text_path = config .debug_output_new_blocks - .then(|| config.store_dir.join("blocks.json")); + .then(|| store_dir.join("blocks.json")); let kura = Arc::new(Self { mode: config.init_mode, @@ -1050,7 +1051,9 @@ mod tests { let temp_dir = TempDir::new().unwrap(); Kura::new(&Config { init_mode: InitMode::Strict, - store_dir: temp_dir.path().to_str().unwrap().into(), + store_dir: iroha_config::base::WithOrigin::inline( + temp_dir.path().to_str().unwrap().into(), + ), debug_output_new_blocks: false, }) .unwrap() diff --git a/core/src/queue.rs b/core/src/queue.rs index 7029f57ff7f..645a5363a04 100644 --- a/core/src/queue.rs +++ b/core/src/queue.rs @@ -92,17 +92,25 @@ pub struct Failure { impl Queue { /// Makes queue from configuration - pub fn from_config(cfg: Config, events_sender: EventsSender) -> Self { + pub fn from_config( + Config { + capacity, + capacity_per_user, + transaction_time_to_live, + future_threshold, + }: Config, + events_sender: EventsSender, + ) -> Self { Self { events_sender, - tx_hashes: ArrayQueue::new(cfg.capacity.get()), + tx_hashes: ArrayQueue::new(capacity.get()), accepted_txs: DashMap::new(), txs_per_user: DashMap::new(), - capacity: cfg.capacity, - capacity_per_user: cfg.capacity_per_user, + capacity, + capacity_per_user, time_source: TimeSource::new_system(), - tx_time_to_live: cfg.transaction_time_to_live, - future_threshold: cfg.future_threshold, + tx_time_to_live: transaction_time_to_live, + future_threshold, } } diff --git a/core/src/smartcontracts/isi/world.rs b/core/src/smartcontracts/isi/world.rs index f8adfbd34ee..f6e978e9f6c 100644 --- a/core/src/smartcontracts/isi/world.rs +++ b/core/src/smartcontracts/isi/world.rs @@ -25,6 +25,7 @@ pub mod isi { query::error::FindError, Level, }; + use iroha_primitives::unique_vec::PushResult; use super::*; @@ -38,10 +39,11 @@ pub mod isi { let peer_id = self.object.id; let world = &mut state_transaction.world; - if !world.trusted_peers_ids.push(peer_id.clone()) { + if let PushResult::Duplicate(duplicate) = world.trusted_peers_ids.push(peer_id.clone()) + { return Err(RepetitionError { instruction_type: InstructionType::Register, - id: IdBox::PeerId(peer_id), + id: IdBox::PeerId(duplicate), } .into()); } diff --git a/core/src/snapshot.rs b/core/src/snapshot.rs index e046b49dba5..572d0810c9b 100644 --- a/core/src/snapshot.rs +++ b/core/src/snapshot.rs @@ -6,7 +6,7 @@ use std::{ time::Duration, }; -use iroha_config::{parameters::actual::Snapshot as Config, snapshot::Mode}; +use iroha_config::{base::WithOrigin, parameters::actual::Snapshot as Config, snapshot::Mode}; use iroha_crypto::HashOf; use iroha_data_model::block::SignedBlock; use iroha_logger::prelude::*; @@ -40,7 +40,7 @@ pub struct SnapshotMaker { /// Frequency at which snapshot is made create_every: Duration, /// Path to the directory where snapshots are stored - store_dir: PathBuf, + store_dir: WithOrigin, /// Hash of the latest block stored in the state latest_block_hash: Option>, } @@ -92,7 +92,8 @@ impl SnapshotMaker { if latest_block_hash != self.latest_block_hash { let state = self.state.clone(); let handle = tokio::task::spawn_blocking(move || -> Result<(), TryWriteError> { - try_write_snapshot(&state, &store_dir) + // TODO: enhance error by attaching `store_dir` parameter origin + try_write_snapshot(&state, store_dir.value()) }); match handle.await { @@ -118,7 +119,7 @@ impl SnapshotMaker { let latest_block_hash = state.view().latest_block_hash(); Some(Self { state, - create_every: config.create_every, + create_every: config.create_every.get(), store_dir: config.store_dir.clone(), latest_block_hash, }) diff --git a/core/src/state.rs b/core/src/state.rs index 2aa616fa6f5..f258cd89911 100644 --- a/core/src/state.rs +++ b/core/src/state.rs @@ -1266,18 +1266,7 @@ impl<'state> StateBlock<'state> { /// Create time event using previous and current blocks fn create_time_event(&self, block: &CommittedBlock) -> TimeEvent { - use iroha_config::parameters::defaults::chain_wide::{ - DEFAULT_BLOCK_TIME, DEFAULT_COMMIT_TIME, - }; - - const DEFAULT_CONSENSUS_ESTIMATION: Duration = - match DEFAULT_BLOCK_TIME.checked_add(match DEFAULT_COMMIT_TIME.checked_div(2) { - Some(x) => x, - None => unreachable!(), - }) { - Some(x) => x, - None => unreachable!(), - }; + use iroha_config::parameters::defaults::chain_wide::CONSENSUS_ESTIMATION as DEFAULT_CONSENSUS_ESTIMATION; let prev_interval = self.latest_block_ref().map(|latest_block| { let header = &latest_block.as_ref().header(); diff --git a/core/src/sumeragi/mod.rs b/core/src/sumeragi/mod.rs index 92c441f17dd..76c0deff391 100644 --- a/core/src/sumeragi/mod.rs +++ b/core/src/sumeragi/mod.rs @@ -152,10 +152,13 @@ impl SumeragiHandle { }); current_topology = match state_view.height() { - 0 => { - assert!(!sumeragi_config.trusted_peers.is_empty()); - Topology::new(sumeragi_config.trusted_peers.clone()) - } + 0 => Topology::new( + sumeragi_config + .trusted_peers + .value() + .clone() + .into_non_empty_vec(), + ), height => { let block_ref = kura.get_block_by_height(height).expect( "Sumeragi could not load block that was reported as present. \ diff --git a/core/test_network/src/lib.rs b/core/test_network/src/lib.rs index e7de53c116c..bf7953ec196 100644 --- a/core/test_network/src/lib.rs +++ b/core/test_network/src/lib.rs @@ -6,7 +6,7 @@ use std::{collections::BTreeMap, ops::Deref, path::Path, sync::Arc, thread}; use eyre::Result; use futures::{prelude::*, stream::FuturesUnordered}; -use iroha::Iroha; +use iroha::{Iroha, ToriiStarted}; use iroha_client::{ client::{Client, QueryOutput}, config::Config as ClientConfig, @@ -14,7 +14,7 @@ use iroha_client::{ }; use iroha_config::parameters::actual::Root as Config; pub use iroha_core::state::StateReadOnly; -use iroha_crypto::KeyPair; +use iroha_crypto::{ExposedPrivateKey, KeyPair}; use iroha_data_model::{query::QueryOutputBox, ChainId}; use iroha_genesis::{GenesisNetwork, RawGenesisBlockFile}; use iroha_logger::{warn, InstrumentFutures}; @@ -150,7 +150,7 @@ impl Network { pub fn get_freeze_status_handles(&self) -> Vec> { self.peers() .filter_map(|peer| peer.iroha.as_ref()) - .map(|iroha| iroha.freeze_status.clone()) + .map(|iroha| iroha.freeze_status().clone()) .collect() } @@ -218,7 +218,7 @@ impl Network { ); let mut config = Config::test(); - config.sumeragi.trusted_peers = + config.sumeragi.trusted_peers.value_mut().others = UniqueVec::from_iter(self.peers().map(|peer| &peer.id).cloned()); let peer = PeerBuilder::new() @@ -273,7 +273,7 @@ impl Network { .collect::>>()?; let mut config = default_config.unwrap_or_else(Config::test); - config.sumeragi.trusted_peers = + config.sumeragi.trusted_peers.value_mut().others = UniqueVec::from_iter(peers.iter().map(|peer| peer.id.clone())); let mut genesis_peer = peers.remove(0); @@ -388,7 +388,7 @@ pub struct Peer { /// Shutdown handle shutdown: Option>, /// Iroha itself - pub iroha: Option, + pub iroha: Option>, /// Temporary directory // Note: last field to be dropped after Iroha (struct fields drops in FIFO RFC 1857) pub temp_dir: Option>, @@ -417,7 +417,10 @@ impl Drop for Peer { impl Peer { /// Returns per peer config with all addresses, keys, and id set up. fn get_config(&self, config: Config) -> Config { - use iroha_config::parameters::actual::{Common, Network, Torii}; + use iroha_config::{ + base::WithOrigin, + parameters::actual::{Common, Network, Torii}, + }; Config { common: Common { @@ -426,11 +429,11 @@ impl Peer { ..config.common }, network: Network { - address: self.p2p_address.clone(), + address: WithOrigin::inline(self.p2p_address.clone()), ..config.network }, torii: Torii { - address: self.api_address.clone(), + address: WithOrigin::inline(self.api_address.clone()), ..config.torii }, ..config @@ -445,7 +448,7 @@ impl Peer { temp_dir: Arc, ) { let mut config = self.get_config(config); - config.kura.store_dir = temp_dir.path().to_str().unwrap().into(); + *config.kura.store_dir.value_mut() = temp_dir.path().to_str().unwrap().into(); let info_span = iroha_logger::info_span!( "test-peer", p2p_addr = %self.p2p_address, @@ -456,10 +459,10 @@ impl Peer { let handle = task::spawn( async move { - let mut iroha = Iroha::new(config, genesis, logger) + let iroha = Iroha::start_network(config, genesis, logger) .await .expect("Failed to start iroha"); - let job_handle = iroha.start_as_task().unwrap(); + let (job_handle, iroha) = iroha.start_torii_as_task(); sender.send(iroha).unwrap(); job_handle.await.unwrap().unwrap(); } @@ -620,7 +623,7 @@ impl PeerBuilder { pub async fn start_with_peer(self, peer: &mut Peer) { let config = self.config.unwrap_or_else(|| { let mut config = Config::test(); - config.sumeragi.trusted_peers = unique_vec![peer.id.clone()]; + config.sumeragi.trusted_peers.value_mut().others = unique_vec![peer.id.clone()]; config }); let genesis = match self.genesis { @@ -778,29 +781,21 @@ impl TestRuntime for Runtime { impl TestConfig for Config { fn test() -> Self { - use iroha_config::{ - base::{FromEnv as _, StdEnv, UnwrapPartial as _}, - parameters::user::{CliContext, RootPartial}, - }; + use iroha_config::base::toml::TomlSource; - let mut layer = iroha::samples::get_user_config( - &UniqueVec::new(), - Some(get_chain_id()), - Some(get_key_pair(Signatory::Peer)), - Some(get_key_pair(Signatory::Genesis)), - ) - .merge(RootPartial::from_env(&StdEnv).expect("test env variables should parse properly")); + let mut raw = iroha::samples::get_config_toml( + <_>::default(), + get_chain_id(), + get_key_pair(Signatory::Peer), + get_key_pair(Signatory::Genesis), + ); let (public_key, private_key) = KeyPair::random().into_parts(); - layer.public_key.set(public_key); - layer.private_key.set(private_key); - - layer - .unwrap_partial() - .expect("should not fail as all fields are present") - .parse(CliContext { - submit_genesis: true, - }) + iroha_config::base::toml::Writer::new(&mut raw) + .write("public_key", public_key) + .write("private_key", ExposedPrivateKey(private_key)); + + Config::from_toml_source(TomlSource::inline(raw)) .expect("Test Iroha config failed to build. This is likely to be a bug.") } diff --git a/p2p/src/network.rs b/p2p/src/network.rs index ec7a3429220..fa98a39bbb7 100644 --- a/p2p/src/network.rs +++ b/p2p/src/network.rs @@ -2,6 +2,7 @@ use std::{ collections::{HashMap, HashSet}, fmt::Debug, + net::ToSocketAddrs, time::Duration, }; @@ -75,7 +76,8 @@ impl NetworkBaseHandle { idle_timeout, }: Config, ) -> Result { - let listener = TcpListener::bind(&listen_addr.to_string()).await?; + // TODO: enhance the error by reporting the origin of `listen_addr` + let listener = TcpListener::bind(listen_addr.value().to_socket_addrs()?.as_slice()).await?; iroha_logger::info!("Network bound to listener"); let (online_peers_sender, online_peers_receiver) = watch::channel(HashSet::new()); let (subscribe_to_peers_messages_sender, subscribe_to_peers_messages_receiver) = @@ -86,7 +88,7 @@ impl NetworkBaseHandle { let (peer_message_sender, peer_message_receiver) = mpsc::channel(1); let (service_message_sender, service_message_receiver) = mpsc::channel(1); let network = NetworkBase { - listen_addr, + listen_addr: listen_addr.into_value(), listener, peers: HashMap::new(), connecting_peers: HashMap::new(), diff --git a/p2p/tests/integration/p2p.rs b/p2p/tests/integration/p2p.rs index 74279167f5e..b73c06f0117 100644 --- a/p2p/tests/integration/p2p.rs +++ b/p2p/tests/integration/p2p.rs @@ -9,6 +9,7 @@ use std::{ use futures::{prelude::*, stream::FuturesUnordered, task::AtomicWaker}; use iroha_config::parameters::actual::Network as Config; +use iroha_config_base::WithOrigin; use iroha_crypto::KeyPair; use iroha_data_model::prelude::PeerId; use iroha_logger::{prelude::*, test_logger}; @@ -41,7 +42,7 @@ async fn network_create() { let public_key = key_pair.public_key().clone(); let idle_timeout = Duration::from_secs(60); let config = Config { - address: address.clone(), + address: WithOrigin::inline(address.clone()), idle_timeout, }; let network = NetworkHandle::start(key_pair, config).await.unwrap(); @@ -152,7 +153,7 @@ async fn two_networks() { info!("Starting first network..."); let address1 = socket_addr!(127.0.0.1:12_005); let config1 = Config { - address: address1.clone(), + address: WithOrigin::inline(address1.clone()), idle_timeout, }; let mut network1 = NetworkHandle::start(key_pair1, config1).await.unwrap(); @@ -160,7 +161,7 @@ async fn two_networks() { info!("Starting second network..."); let address2 = socket_addr!(127.0.0.1:12_010); let config2 = Config { - address: address2.clone(), + address: WithOrigin::inline(address2.clone()), idle_timeout, }; let network2 = NetworkHandle::start(key_pair2, config2).await.unwrap(); @@ -298,7 +299,7 @@ async fn start_network( let PeerId { address, .. } = peer.clone(); let idle_timeout = Duration::from_secs(60); let config = Config { - address, + address: WithOrigin::inline(address), idle_timeout, }; let mut network = NetworkHandle::start(key_pair, config).await.unwrap(); diff --git a/primitives/src/unique_vec.rs b/primitives/src/unique_vec.rs index 0eb1ea5aa26..5caf4422e62 100644 --- a/primitives/src/unique_vec.rs +++ b/primitives/src/unique_vec.rs @@ -71,16 +71,23 @@ impl UniqueVec { } } +/// A result for [`UniqueVec::push`] +#[derive(Debug)] +pub enum PushResult { + /// The element was pushed into the vec + Ok, + /// The element is already contained in the vec + Duplicate(T), +} + impl UniqueVec { /// Push `value` to [`UniqueVec`] if it is not already present. - /// - /// Returns `true` if value was pushed and `false` if not. - pub fn push(&mut self, value: T) -> bool { + pub fn push(&mut self, value: T) -> PushResult { if self.contains(&value) { - false + PushResult::Duplicate(value) } else { self.0.push(value); - true + PushResult::Ok } } } @@ -317,13 +324,13 @@ mod tests { #[test] fn push_returns_true_if_value_is_unique() { let mut unique_vec = unique_vec![1, 3, 4]; - assert!(unique_vec.push(2)); + assert!(matches!(unique_vec.push(2), PushResult::Ok)); } #[test] fn push_returns_false_if_value_is_not_unique() { let mut unique_vec = unique_vec![1, 2, 3]; - assert!(!unique_vec.push(1)); + assert!(matches!(unique_vec.push(1), PushResult::Duplicate(1))); } #[test] diff --git a/tools/kagami/src/genesis.rs b/tools/kagami/src/genesis.rs index 1902d8aecc7..3cbce59a939 100644 --- a/tools/kagami/src/genesis.rs +++ b/tools/kagami/src/genesis.rs @@ -1,11 +1,7 @@ use std::path::PathBuf; use clap::{Parser, Subcommand}; -use iroha_config::parameters::defaults::chain_wide::{ - DEFAULT_BLOCK_TIME, DEFAULT_COMMIT_TIME, DEFAULT_IDENT_LENGTH_LIMITS, DEFAULT_MAX_TXS, - DEFAULT_METADATA_LIMITS, DEFAULT_TRANSACTION_LIMITS, DEFAULT_WASM_FUEL_LIMIT, - DEFAULT_WASM_MAX_MEMORY_BYTES, -}; +use iroha_config::parameters::defaults::chain_wide as chain_wide_defaults; use iroha_data_model::{ asset::{AssetDefinitionId, AssetValueType}, metadata::Limits, @@ -149,38 +145,56 @@ pub fn generate_default( let parameter_defaults = ParametersBuilder::new() .add_parameter( MAX_TRANSACTIONS_IN_BLOCK, - Numeric::new(DEFAULT_MAX_TXS.get().into(), 0), + Numeric::new(chain_wide_defaults::MAX_TXS.get().into(), 0), + )? + .add_parameter( + BLOCK_TIME, + Numeric::new(chain_wide_defaults::BLOCK_TIME.as_millis(), 0), )? - .add_parameter(BLOCK_TIME, Numeric::new(DEFAULT_BLOCK_TIME.as_millis(), 0))? .add_parameter( COMMIT_TIME_LIMIT, - Numeric::new(DEFAULT_COMMIT_TIME.as_millis(), 0), + Numeric::new(chain_wide_defaults::COMMIT_TIME.as_millis(), 0), + )? + .add_parameter(TRANSACTION_LIMITS, chain_wide_defaults::TRANSACTION_LIMITS)? + .add_parameter( + WSV_DOMAIN_METADATA_LIMITS, + chain_wide_defaults::METADATA_LIMITS, )? - .add_parameter(TRANSACTION_LIMITS, DEFAULT_TRANSACTION_LIMITS)? - .add_parameter(WSV_DOMAIN_METADATA_LIMITS, DEFAULT_METADATA_LIMITS)? .add_parameter( WSV_ASSET_DEFINITION_METADATA_LIMITS, - DEFAULT_METADATA_LIMITS, + chain_wide_defaults::METADATA_LIMITS, + )? + .add_parameter( + WSV_ACCOUNT_METADATA_LIMITS, + chain_wide_defaults::METADATA_LIMITS, + )? + .add_parameter( + WSV_ASSET_METADATA_LIMITS, + chain_wide_defaults::METADATA_LIMITS, + )? + .add_parameter( + WSV_TRIGGER_METADATA_LIMITS, + chain_wide_defaults::METADATA_LIMITS, + )? + .add_parameter( + WSV_IDENT_LENGTH_LIMITS, + chain_wide_defaults::IDENT_LENGTH_LIMITS, )? - .add_parameter(WSV_ACCOUNT_METADATA_LIMITS, DEFAULT_METADATA_LIMITS)? - .add_parameter(WSV_ASSET_METADATA_LIMITS, DEFAULT_METADATA_LIMITS)? - .add_parameter(WSV_TRIGGER_METADATA_LIMITS, DEFAULT_METADATA_LIMITS)? - .add_parameter(WSV_IDENT_LENGTH_LIMITS, DEFAULT_IDENT_LENGTH_LIMITS)? .add_parameter( EXECUTOR_FUEL_LIMIT, - Numeric::new(DEFAULT_WASM_FUEL_LIMIT.into(), 0), + Numeric::new(chain_wide_defaults::WASM_FUEL_LIMIT.into(), 0), )? .add_parameter( EXECUTOR_MAX_MEMORY, - Numeric::new(DEFAULT_WASM_MAX_MEMORY_BYTES.into(), 0), + Numeric::new(chain_wide_defaults::WASM_MAX_MEMORY_BYTES.into(), 0), )? .add_parameter( WASM_FUEL_LIMIT, - Numeric::new(DEFAULT_WASM_FUEL_LIMIT.into(), 0), + Numeric::new(chain_wide_defaults::WASM_FUEL_LIMIT.into(), 0), )? .add_parameter( WASM_MAX_MEMORY, - Numeric::new(DEFAULT_WASM_MAX_MEMORY_BYTES.into(), 0), + Numeric::new(chain_wide_defaults::WASM_MAX_MEMORY_BYTES.into(), 0), )? .into_create_parameters(); diff --git a/tools/swarm/src/compose.rs b/tools/swarm/src/compose.rs index cb4a77d8368..e1e88bd8306 100644 --- a/tools/swarm/src/compose.rs +++ b/tools/swarm/src/compose.rs @@ -544,8 +544,8 @@ mod tests { }; use iroha_config::{ - base::{FromEnv, TestEnv, UnwrapPartial}, - parameters::user::{CliContext, RootPartial}, + base::{env::MockEnv, read::ConfigReader}, + parameters::user::Root as UserConfig, }; use iroha_crypto::KeyPair; use iroha_primitives::addr::{socket_addr, SocketAddr}; @@ -563,7 +563,7 @@ mod tests { } } - impl From for TestEnv { + impl From for MockEnv { fn from(peer_env: FullPeerEnv) -> Self { let json = serde_json::to_string(&peer_env).expect("Must be serializable"); let env: HashMap<_, String> = @@ -572,7 +572,7 @@ mod tests { } } - impl From for TestEnv { + impl From for MockEnv { fn from(value: CompactPeerEnv) -> Self { let full: FullPeerEnv = value.into(); full.into() @@ -582,7 +582,7 @@ mod tests { #[test] fn default_config_with_swarm_env_is_exhaustive() { let keypair = KeyPair::random(); - let env: TestEnv = CompactPeerEnv { + let env: MockEnv = CompactPeerEnv { chain_id: ChainId::from("00000000-0000-0000-0000-000000000000"), key_pair: keypair.clone(), genesis_public_key: keypair.public_key().clone(), @@ -600,14 +600,10 @@ mod tests { } .into(); - let _cfg = RootPartial::from_env(&env) - .expect("valid env") - .unwrap_partial() - .expect("should not fail as input has all required fields") - .parse(CliContext { - submit_genesis: true, - }) - .expect("should not fail as input is valid"); + let _ = ConfigReader::new() + .with_env(env.clone()) + .read_and_complete::() + .expect("config in env should be exhaustive"); assert_eq!(env.unvisited(), HashSet::new()); } diff --git a/torii/src/lib.rs b/torii/src/lib.rs index 6ba4fa98b5d..a987608ee3f 100644 --- a/torii/src/lib.rs +++ b/torii/src/lib.rs @@ -85,7 +85,7 @@ impl Torii { state, #[cfg(feature = "telemetry")] metrics_reporter, - address: config.address, + address: config.address.into_value(), transaction_max_content_length: config.max_content_len_bytes, } } @@ -243,27 +243,24 @@ impl Torii { fn start_api(self: Arc) -> eyre::Result>> { let torii_address = &self.address; - let mut handles = vec![]; - match torii_address.to_socket_addrs() { - Ok(addrs) => { - for addr in addrs { - let torii = Arc::clone(&self); + let handles = torii_address + .to_socket_addrs()? + .map(|addr| { + let torii = Arc::clone(&self); - let api_router = torii.create_api_router(); - let signal_fut = async move { torii.notify_shutdown.notified().await }; - let (_, serve_fut) = - warp::serve(api_router).bind_with_graceful_shutdown(addr, signal_fut); + let api_router = torii.create_api_router(); + let signal_fut = async move { torii.notify_shutdown.notified().await }; + // FIXME: warp panics if fails to bind! + // handle this properly, report address origin after Axum + // migration: https://github.com/hyperledger/iroha/issues/3776 + let (_, serve_fut) = + warp::serve(api_router).bind_with_graceful_shutdown(addr, signal_fut); - handles.push(task::spawn(serve_fut)); - } + task::spawn(serve_fut) + }) + .collect(); - Ok(handles) - } - Err(error) => { - iroha_logger::error!(%torii_address, %error, "API address configuration parse error"); - Err(eyre::Error::new(error)) - } - } + Ok(handles) } /// To handle incoming requests `Torii` should be started first.