From 9f91ee2cbeb8c89cfda36e1d959a3c61b6db9f14 Mon Sep 17 00:00:00 2001 From: breis Date: Sun, 19 Jul 2026 22:19:00 -0400 Subject: [PATCH 1/7] build(cargo): commit Cargo.lock against the pinned git rev Cargo.lock was gitignored with a rationale ("excluded until the edgecommons pin is bumped to the UNS-core rev") that no longer held -- the pin moved to the D-U28 rev in 49d11fd, so the gate on committing it had already been cleared. Per the org-level lockfile-commit policy (SD-B of the component-baseline remediation plan): regenerated with the local .cargo/config.toml [patch] override inactive, so the lock records the git dependency (edgecommons?rev=36a70c48...) rather than a local sibling path -- resolvable on a fresh clone and in CI. Verified: cargo build/test/clippy all pass against this lock with the override removed from the resolution path (isolated build, no ancestor .cargo/config.toml), on both Windows and WSL/Linux. This also gives issue #10's Docker-build report a lockfile CI can actually see. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01DxV5wFi7LCratBERVT6aiL --- .gitignore | 4 - Cargo.lock | 4919 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 4919 insertions(+), 4 deletions(-) create mode 100644 Cargo.lock diff --git a/.gitignore b/.gitignore index 75dc691..1826557 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,3 @@ greengrass-build/ # local-dev sibling override (build against ../core/libs/rust instead of the pinned git rev) .cargo/ - -# Cargo.lock excluded until the edgecommons pin is bumped to the UNS-core rev on main and regenerated -# (the current lock would record the local .cargo [patch] path, invalid on a fresh clone / in CI). -/Cargo.lock diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..6f980ed --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4919 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "apache-avro" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aef82843a0ec9f8b19567445ad2421ceeb1d711514384bdd3d49fe37102ee13" +dependencies = [ + "bigdecimal", + "crc32fast", + "digest 0.10.7", + "libflate", + "log", + "num-bigint", + "quad-rand", + "rand", + "regex-lite", + "serde", + "serde_bytes", + "serde_json", + "snap", + "strum", + "strum_macros", + "thiserror 1.0.69", + "typed-builder", + "uuid", + "zstd", +] + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arrow" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5ec52ba94edeed950e4a41f75d35376df196e8cb04437f7280a5aa49f20f796" +dependencies = [ + "arrow-arith", + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-ord", + "arrow-row", + "arrow-schema", + "arrow-select", + "arrow-string", +] + +[[package]] +name = "arrow-arith" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc766fdacaf804cb10c7c70580254fcdb5d55cdfda2bc57b02baf5223a3af9e" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "num", +] + +[[package]] +name = "arrow-array" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12fcdb3f1d03f69d3ec26ac67645a8fe3f878d77b5ebb0b15d64a116c212985" +dependencies = [ + "ahash", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "half", + "hashbrown 0.15.5", + "num", +] + +[[package]] +name = "arrow-buffer" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "263f4801ff1839ef53ebd06f99a56cecd1dbaf314ec893d93168e2e860e0291c" +dependencies = [ + "bytes", + "half", + "num", +] + +[[package]] +name = "arrow-cast" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede6175fbc039dfc946a61c1b6d42fd682fcecf5ab5d148fbe7667705798cac9" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "atoi", + "base64", + "chrono", + "half", + "lexical-core", + "num", + "ryu", +] + +[[package]] +name = "arrow-data" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61cfdd7d99b4ff618f167e548b2411e5dd2c98c0ddebedd7df433d34c20a4429" +dependencies = [ + "arrow-buffer", + "arrow-schema", + "half", + "num", +] + +[[package]] +name = "arrow-ipc" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ff528658b521e33905334723b795ee56b393dbe9cf76c8b1f64b648c65a60c" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "flatbuffers", +] + +[[package]] +name = "arrow-ord" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0a3334a743bd2a1479dbc635540617a3923b4b2f6870f37357339e6b5363c21" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", +] + +[[package]] +name = "arrow-row" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d1d7a7291d2c5107e92140f75257a99343956871f3d3ab33a7b41532f79cb68" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "half", +] + +[[package]] +name = "arrow-schema" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cfaf5e440be44db5413b75b72c2a87c1f8f0627117d110264048f2969b99e9" + +[[package]] +name = "arrow-select" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69efcd706420e52cd44f5c4358d279801993846d1c2a8e52111853d61d55a619" +dependencies = [ + "ahash", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "num", +] + +[[package]] +name = "arrow-string" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21546b337ab304a32cfc0770f671db7411787586b45b78b4593ae78e64e2b03" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "memchr", + "num", + "regex", + "regex-syntax", +] + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.1", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-config" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47712fde1909402600ccfbb26e47d482d2e58bb9e9e603d9f17e67cc435a6319" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 1.4.2", + "sha1", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93964ffdaf57857f544be3666a5f57570bb699e934700f11b49708f61bb556e" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-greengrass-component-sdk" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27784b53a82e3063993a5825e2c317624b6aa92e1cd5b0fbafb1bfcab9e59e2f" +dependencies = [ + "bindgen", + "cc", +] + +[[package]] +name = "aws-lc-rs" +version = "1.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "aws-runtime" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7816e98ee912159f45d307e5ee6bfea4a335a55aee15f7f3e32f81a6f3000f1d" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "bytes-utils", + "fastrand", + "http 1.4.2", + "http-body 1.1.0", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-cloudwatch" +version = "1.120.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4b02c77603855d7f7fc35f56f0be648200c607ad3072e86df9a27f1c4ac2a6c" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-cbor", + "aws-smithy-compression", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-types", + "fastrand", + "flate2", + "http 0.2.12", + "http 1.4.2", + "http-body 1.1.0", + "http-body-util", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-kinesis" +version = "1.110.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5840be360c11a2a99f714401ed87497018c5b96cb315e03dc3cf06d5fcc834db" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.2", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.103.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0469f435f645ad2162cfb463b15bde37115966ee3acf2d87fb4871ee309b8401" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.2", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.105.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085faefb253f770655e162b9304321e62a1e71adf7f019ee1f4454228a377b3a" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.2", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.108.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c72b08911d8128dd360fe1b22a9fec0fa8b552dde8ec828dcf20ef5ec974e9f" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "http 1.4.2", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "723c2234ad7511ceef63eab016b7ba6ff7c55590fefb96fa8467af014a07309f" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "form_urlencoded", + "hex", + "hmac", + "http 0.2.12", + "http 1.4.2", + "percent-encoding", + "sha2", + "time", + "tracing", +] + +[[package]] +name = "aws-smithy-async" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02e407fb3b54891734224b9ffac8a71fdd35f542500fa1af95754a6b2beb316" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-cbor" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f069169abdc367533ea89b869b7dfaadd4134a9bc287c13dbe5ff364ff13228" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "minicbor", + "num-bigint", +] + +[[package]] +name = "aws-smithy-compression" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016ef334a13c53bd75184bb8f89b0fb2aa4d56cb3cfc478ab3f05f13654b60a1" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "flate2", + "futures-util", + "http 1.4.2", + "http-body 1.1.0", + "http-body-util", + "pin-project-lite", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9381123ab62d20c13082b151f30f962a3b112b727345394536dfa39a482944" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37843d9add67c3aff5856f409c6dc315d3cdff60f9c0cb5b670dab1e9920306d" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.4.2", + "http-body 1.1.0", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635d23afda0a6ab48d666c4d447c4873e8d1e83518a2be2093122397e50b838e" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.15", + "http 0.2.12", + "http 1.4.2", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.10.1", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.9", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.42", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dc65a121adb4b33729919fcfa14fa36fb33c1555a8f06bb0e2188dbfdc1d9ef" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e86338c869539a581bf161247762a6e87f92c5c075060057b5ed6d06632ed0c" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd22a6ba36e3f113cb8d5b3d1fe0ed31c76ee608ef63322d753bb8d2c9479e77" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea94a9ff8464016338c851e24b472d7131c388c88898a502e781815b2ee6045" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.2", + "http-body 0.4.6", + "http-body 1.1.0", + "http-body-util", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ed1ebe6e0a95ea84570225f5a8208dec4b8f77e61a9b0d6f51773fcb4612f0" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api-macros", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.4.2", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-runtime-api-macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "221eaa237ddf1ca79b60d1372aad77e47f9c0ea5b3ce5099da8c61d027dc77b3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "aws-smithy-schema" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56e0a4e53127a632224e43633b0fe045fa9e1e3cfc68b9830f1115e103f910" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "http 1.4.2", +] + +[[package]] +name = "aws-smithy-types" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6dc683efb34b9e755675b37fedbe0103141e5b6df7bdc9eb6967756a8c167d8" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.4.2", + "http-body 0.4.6", + "http-body 1.1.0", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea3f68eec3607f02acd24067969ce2abc6ba16aa7d5ce59ca450ed2fb5f78957" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e957a6c6dbce82b7a91f44231c09273159703769f447cbe85e854dfe9cf67f86" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "rustc_version", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "bigdecimal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.13.1", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.119", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + +[[package]] +name = "bstr" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" +dependencies = [ + "memchr", + "serde_core", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "cc" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link 0.2.1", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32c" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "dary_heap" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1e3a325bc115f096c8b77bbf027a7c2592230e70be2d985be950d3d5e60ebe" + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "edgecommons" +version = "0.2.0" +source = "git+https://github.com/edgecommons/edgecommons.git?rev=36a70c48b65b35f77bfab70d3a73869debdfc407#36a70c48b65b35f77bfab70d3a73869debdfc407" +dependencies = [ + "arc-swap", + "async-trait", + "aws-config", + "aws-greengrass-component-sdk", + "aws-sdk-cloudwatch", + "base64", + "clap", + "edgestreamlog", + "jsonschema", + "notify", + "prost", + "prost-build", + "protoc-bin-vendored", + "rand", + "regex", + "rumqttc", + "serde", + "serde_json", + "sysinfo", + "thiserror 2.0.19", + "time", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", + "windows-sys 0.61.2", +] + +[[package]] +name = "edgestreamlog" +version = "0.1.0" +source = "git+https://github.com/edgecommons/edgecommons.git?rev=36a70c48b65b35f77bfab70d3a73869debdfc407#36a70c48b65b35f77bfab70d3a73869debdfc407" +dependencies = [ + "apache-avro", + "arrow", + "aws-config", + "aws-sdk-kinesis", + "base64", + "crc32c", + "parquet", + "prost", + "prost-build", + "protoc-bin-vendored", + "rdkafka", + "serde", + "serde_json", + "thiserror 2.0.19", + "time", + "tokio", + "tracing", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fancy-regex" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e1dacd0d2082dfcf1351c4bdd566bbe89a2b263235a2b50058f1e130a47277" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flatbuffers" +version = "24.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f1baf0dbf96932ec9a3038d57900329c015b0bfb7b63d904f3bc27e2b02a096" +dependencies = [ + "bitflags 1.3.2", + "rustc_version", +] + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fluent-uri" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.9", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fraction" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872" +dependencies = [ + "lazy_static", + "num", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.2", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http 1.4.2", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.2", + "http-body 1.1.0", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2 0.4.15", + "http 1.4.2", + "http-body 1.1.0", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http 1.4.2", + "hyper 1.10.1", + "hyper-util", + "rustls 0.23.42", + "rustls-native-certs", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http 1.4.2", + "http-body 1.1.0", + "hyper 1.10.1", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.5", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "inotify" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "153be1941a183ec9ccd095ddbe17a8b8d435ef6c76e9e02451b933c3999af2c8" +dependencies = [ + "bitflags 2.13.1", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" +dependencies = [ + "libc", +] + +[[package]] +name = "integer-encoding" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.19", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "jsonschema" +version = "0.46.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0a699d3e77675e6aa4bfffe3b907c8b5f7ed3241f9965bffb25475ad4b08d05" +dependencies = [ + "ahash", + "bytecount", + "data-encoding", + "email_address", + "fancy-regex", + "fraction", + "getrandom 0.3.4", + "idna", + "itoa", + "jsonschema-regex", + "num-cmp", + "num-traits", + "percent-encoding", + "referencing", + "regex", + "reqwest", + "rustls 0.23.42", + "serde", + "serde_json", + "unicode-general-category", + "uuid-simd", +] + +[[package]] +name = "jsonschema-regex" +version = "0.46.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbd1086b01b9349fd4ef9a07433965af64c8ce8159abe633a189e4ff817bd13" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "kqueue" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags 2.13.1", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lexical-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56" +dependencies = [ + "lexical-parse-integer", + "lexical-util", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "lexical-util" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17" + +[[package]] +name = "lexical-write-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361" +dependencies = [ + "lexical-util", + "lexical-write-integer", +] + +[[package]] +name = "lexical-write-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libflate" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd96e993e5f3368b0cb8497dae6c860c22af8ff18388c61c6c0b86c58d86b5df" +dependencies = [ + "adler32", + "crc32fast", + "dary_heap", + "libflate_lz77", + "no_std_io2", +] + +[[package]] +name = "libflate_lz77" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff7a10e427698aef6eef269482776debfef63384d30f13aad39a1a95e0e098fd" +dependencies = [ + "hashbrown 0.16.1", + "no_std_io2", + "rle-decode-fast", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libz-sys" +version = "1.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lua-src" +version = "547.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edaf29e3517b49b8b746701e5648ccb5785cde1c119062cbabbc5d5cd115e42" +dependencies = [ + "cc", +] + +[[package]] +name = "luajit-src" +version = "210.5.12+a4f56a4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a8e7962a5368d5f264d045a5a255e90f9aa3fc1941ae15a8d2940d42cac671" +dependencies = [ + "cc", + "which", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "micromap" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74" + +[[package]] +name = "minicbor" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b7a5041e12946f8b7d3f5a9d96383a19d694b9335457c522be7815b9abafb02" +dependencies = [ + "half", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mlua" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1f5f8fbebc7db5f671671134b9321c4b9aa9adeafccfd9a8c020ae45c6a35d0" +dependencies = [ + "bstr", + "either", + "erased-serde", + "mlua-sys", + "num-traits", + "parking_lot", + "rustc-hash", + "rustversion", + "serde", + "serde-value", +] + +[[package]] +name = "mlua-sys" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380c1f7e2099cafcf40e51d3a9f20a346977587aa4d012eae1f043149a728a93" +dependencies = [ + "cc", + "cfg-if", + "lua-src", + "luajit-src", + "pkg-config", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" +dependencies = [ + "spin 0.5.2", +] + +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.13.1", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "parquet" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb15796ac6f56b429fd99e33ba133783ad75b27c36b4b5ce06f1f82cc97754e" +dependencies = [ + "ahash", + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-ipc", + "arrow-schema", + "arrow-select", + "base64", + "bytes", + "chrono", + "flate2", + "half", + "hashbrown 0.15.5", + "num", + "num-bigint", + "paste", + "seq-macro", + "snap", + "thrift", + "twox-hash", + "zstd", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "portable-atomic" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +dependencies = [ + "heck", + "itertools 0.14.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.119", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + +[[package]] +name = "protoc-bin-vendored" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c381df33c98266b5f08186583660090a4ffa0889e76c7e9a5e175f645a67fa" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-s390_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-aarch_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c350df4d49b5b9e3ca79f7e646fde2377b199e13cfa87320308397e1f37e1a4c" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55a63e6c7244f19b5c6393f025017eb5d793fd5467823a099740a7a4222440c" + +[[package]] +name = "protoc-bin-vendored-linux-s390_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dba5565db4288e935d5330a07c264a4ee8e4a5b4a4e6f4e83fad824cc32f3b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8854774b24ee28b7868cd71dccaae8e02a2365e67a4a87a6cd11ee6cdbdf9cf5" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b38b07546580df720fa464ce124c4b03630a6fb83e05c336fea2a241df7e5d78" + +[[package]] +name = "protoc-bin-vendored-macos-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89278a9926ce312e51f1d999fee8825d324d603213344a9a706daa009f1d8092" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81745feda7ccfb9471d7a4de888f0652e806d5795b61480605d4943176299756" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3" + +[[package]] +name = "quad-rand" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a651516ddc9168ebd67b24afd085a718be02f8858fe406591b013d101ce2f40" + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rdkafka" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1beea247b9a7600a81d4cc33f659ce1a77e1988323d7d2809c7ed1c21f4c316d" +dependencies = [ + "futures-channel", + "futures-util", + "libc", + "log", + "rdkafka-sys", + "serde", + "serde_derive", + "serde_json", + "slab", + "tokio", +] + +[[package]] +name = "rdkafka-sys" +version = "4.10.0+2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e234cf318915c1059d4921ef7f75616b5219b10b46e9f3a511a15eb4b56a3f77" +dependencies = [ + "cmake", + "libc", + "libz-sys", + "num_enum", + "pkg-config", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.1", +] + +[[package]] +name = "referencing" +version = "0.46.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbf332a2f81899f6836f22c03da73dae8a664c32e3016b84692c23cddadc95d" +dependencies = [ + "ahash", + "fluent-uri", + "getrandom 0.3.4", + "hashbrown 0.16.1", + "itoa", + "micromap", + "parking_lot", + "percent-encoding", + "serde_json", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.4.15", + "http 1.4.2", + "http-body 1.1.0", + "http-body-util", + "hyper 1.10.1", + "hyper-rustls 0.27.9", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "rustls 0.23.42", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls 0.26.4", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "rhai" +version = "1.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4dd0f8c36625202a4ba553c416c19b719947cd2a31d1bda06126e4a5727daf" +dependencies = [ + "ahash", + "bitflags 2.13.1", + "no-std-compat", + "num-traits", + "once_cell", + "rhai_codegen", + "serde", + "smallvec", + "smartstring", + "thin-vec", + "web-time", +] + +[[package]] +name = "rhai_codegen" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd3a7535e50bf36857e7be7bec276d334e8c2dfa469c2201226fd01638ea5ca" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rle-decode-fast" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" + +[[package]] +name = "rumqttc" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0feff8d882bff0b2fddaf99355a10336d43dd3ed44204f85ece28cf9626ab519" +dependencies = [ + "bytes", + "fixedbitset", + "flume", + "futures-util", + "log", + "rustls-native-certs", + "rustls-pemfile", + "rustls-webpki 0.102.8", + "thiserror 2.0.19", + "tokio", + "tokio-rustls 0.26.4", + "tokio-stream", + "tokio-util", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.103.13", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls 0.23.42", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.13", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "seq-macro" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.1", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "serde", + "static_assertions", + "version_check", +] + +[[package]] +name = "snap" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "199905e6153d6405f9728fe44daace35f8f837bbf830bb6e85fbd5828709a886" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5edbec4ed188954a10c12c038215f8ce7606b2d5c973cd8dc43e8795065c5f2f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "sysinfo" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252800745060e7b9ffb7b2badbd8b31cfa4aa2e61af879d0a3bf2a317c20217d" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows", +] + +[[package]] +name = "telemetry-processor" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "edgecommons", + "mlua", + "rhai", + "serde", + "serde_json", + "smallvec", + "tempfile", + "tokio", + "tracing", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thin-vec" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0f7e269b48f0a7dd0146680fa24b50cc67fc0373f086a5b2f99bd084639b482" +dependencies = [ + "serde", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl 2.0.19", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.1", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "thrift" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e54bc85fc7faa8bc175c4bab5b92ba8d9a3ce893d0e9f42cc455c8ab16a9e09" +dependencies = [ + "byteorder", + "integer-encoding", + "ordered-float", +] + +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.5", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.42", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.1", + "bytes", + "futures-util", + "http 1.4.2", + "http-body 1.1.0", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + +[[package]] +name = "typed-builder" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06fbd5b8de54c5f7c91f6fe4cebb949be2125d7758e630bb58b1d831dbce600" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9534daa9fd3ed0bd911d462a37f172228077e7abf18c18a5f67199d959205f8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-general-category" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "7.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" +dependencies = [ + "either", + "env_home", + "rustix", + "winsafe", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] From 934651b5af73b102bc21f4e3532d436abe5270b5 Mon Sep 17 00:00:00 2001 From: breis Date: Sun, 19 Jul 2026 22:19:12 -0400 Subject: [PATCH 2/7] fix(license): reconcile Cargo.toml license to BUSL-1.1 Cargo.toml declared license = "Apache-2.0", a stale leftover that disagreed with the actual LICENSE file (BSL 1.1 / SPDX BUSL-1.1) per org policy: all EdgeCommons repos are licensed BUSL-1.1 throughout. Also sweeps a stale comment above the edgecommons git dependency that still pointed at a `../core-monorepo/libs/rust` local-dev override path -- the current sibling layout (and the org convention) is a gitignored .cargo/config.toml [patch] override at ../core/libs/rust, not a hand-edited Cargo.toml path dependency. Fixes #9 Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01DxV5wFi7LCratBERVT6aiL --- Cargo.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 42f28ea..bfa097f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "telemetry-processor" version = "0.1.0" edition = "2021" rust-version = "1.85" -license = "Apache-2.0" +license = "BUSL-1.1" description = "Reference Rust processing component for edgecommons/edgecommons: subscribe → filter/sample/aggregate/project/script → forward (local / northbound / durable stream → Kinesis/Kafka/file)." publish = false @@ -46,7 +46,8 @@ scripting-lua = ["dep:mlua"] # repo standalone needs read access to that repo. The fetch goes through the git CLI (see # .cargo/config.toml); locally it uses your gh/git credential helper, and in CI the reusable workflow # rewrites this https URL with the EDGECOMMONS_READ_TOKEN PAT. For local development against an -# unpushed change, temporarily swap this for `path = "../core-monorepo/libs/rust"`. +# unpushed change, add a gitignored `.cargo/config.toml` `[patch]` override pointing at +# `../core/libs/rust` (the sibling checkout) instead of editing this pin — see CLAUDE.md. edgecommons = { git = "https://github.com/edgecommons/edgecommons.git", rev = "36a70c48b65b35f77bfab70d3a73869debdfc407", default-features = false } tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal", "time", "sync"] } serde = { version = "1", features = ["derive"] } From e2b08fec6488d1f4934f1cadba8434e35cef3f95 Mon Sep 17 00:00:00 2001 From: breis Date: Sun, 19 Jul 2026 22:19:34 -0400 Subject: [PATCH 3/7] feat(ci): enforce the 90% line-coverage gate; split the untestable composition root out of app.rs Adds a `coverage` job to ci.yml (cargo llvm-cov --fail-under-lines 90, the same feature set the build job uses) -- the repo's ~94%/91% coverage figures were previously validated locally only; CI never gated them. Getting there honestly required a real testability fix, not just an exclusion list: `ProcessorApp::start`/`build_route`/`run` (src/app.rs) obtain a live Config/Arc/EventsFacade/Arc/Arc from a real &EdgeCommons, and none of those types has a public or test constructor outside the edgecommons crate -- so this file cannot be unit-tested without a live runtime (mirrors why the protocol-adapter templates exclude their `supervisor.rs` seam, for the analogous reason of a live device driver). Everything that does NOT need &EdgeCommons directly is moved to a new src/dispatch.rs, which IS fully unit-tested: - the self-echo guard + fan-out handler (self_subscribe) - the get-stats/flush/pause/resume command verbs + panel registration - the two edge-console panels (`overview`, `routes`) -- issue #11's optional P2-6 enhancement, straightforward via the existing register_panel, so implemented rather than deferred; scope: "component" (no console instance dimension for a processor) - script-output-topic validation This crate's own src/test_support.rs adds a downstream MessagingService fake (FakeMessaging), since the library's own testutil::RecordingMessaging is pub(crate) and unreachable here. src/observe.rs's EvtEmitter gains a test-only `recording()` constructor for the same reason -- EventsFacade has no public constructor -- mirroring file-replicator's Events::recording_events precedent for the identical constraint. src/proc/route.rs's Dispatcher (already EdgeCommons-free) gains full local/northbound/stream-target/restamp/ failure-evt test coverage using the new fakes. The coverage job excludes exactly main.rs, app.rs, and test_support.rs (the last is test-only support code, not product logic -- the same treatment file-replicator gives its own testutil.rs). Measured at 92.48% lines (96 tests, cargo test/clippy -D warnings/llvm-cov all green, identical results on native Windows and WSL/Linux -- no undercount discrepancy this time). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01DxV5wFi7LCratBERVT6aiL --- .github/workflows/ci.yml | 48 ++++ src/app.rs | 328 ++++--------------------- src/dispatch.rs | 518 +++++++++++++++++++++++++++++++++++++++ src/main.rs | 3 + src/observe.rs | 75 +++++- src/proc/route.rs | 173 ++++++++++++- src/test_support.rs | 166 +++++++++++++ 7 files changed, 1019 insertions(+), 292 deletions(-) create mode 100644 src/dispatch.rs create mode 100644 src/test_support.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4f6312..a024b4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,3 +21,51 @@ jobs: # ubuntu runner has gcc) so both script engines are compiled + tested in CI. rust-features: "streaming,streaming-file-parquet,streaming-file-avro,scripting-lua" secrets: inherit + + coverage: + # The 90% line-coverage gate (org validation matrix / AGENTS.md), actually RUN here (the + # reusable component-ci only does build/test/clippy). This repo has no live-infra driver seam of + # its own — the Kinesis/Kafka/file-sink clients live in the `edgecommons`/`edgestreamlog` + # library, not here — so the genuine untestable surface is narrower than a southbound adapter's: + # only `main.rs` (the runtime bootstrap shim) and `app.rs` (`ProcessorApp::start`/`build_route`/ + # `run`, the composition root that must obtain a live `Config`/`Arc`/ + # `EventsFacade`/`Arc`/`Arc` from a real `EdgeCommons` — none of + # which has a public or test constructor outside the `edgecommons` crate) are excluded, each + # pinned to its reason in `src/app.rs`'s module doc and `AGENTS.md`. `test_support.rs` (this + # crate's own downstream `MessagingService` fake, the analog of the library's crate-private + # `testutil::RecordingMessaging`) is test-only support code, not production logic, and is + # excluded the same way `file-replicator` excludes its `testutil.rs`. Every piece of logic that + # does NOT need a live `EdgeCommons` directly — the fan-out handler, the command/panel + # registration, the route dispatcher, the pipeline stages, config parsing — is unit-tested and + # stays in the denominator. Do not lower the gate or exclude testable code to pass it — add tests. + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - name: Install Rust toolchain + run: rustup toolchain install stable --profile minimal --component llvm-tools-preview + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + - name: Authenticate to the private edgecommons git dependency + env: + # Same org read token the reusable workflow uses (secrets: inherit); the repo must be on + # that secret's allow-list. Rewrites the https git URL so cargo can fetch the private dep. + EDGECOMMONS_READ_TOKEN: ${{ secrets.EDGECOMMONS_READ_TOKEN }} + run: | + git config --global \ + url."https://x-access-token:${EDGECOMMONS_READ_TOKEN}@github.com/edgecommons/edgecommons.git".insteadOf \ + "https://github.com/edgecommons/edgecommons.git" + git config --global \ + url."https://x-access-token:${EDGECOMMONS_READ_TOKEN}@github.com/edgecommons/edgecommons".insteadOf \ + "https://github.com/edgecommons/edgecommons" + git config --global \ + url."https://x-access-token:${EDGECOMMONS_READ_TOKEN}@github.com/".insteadOf \ + "https://github.com/" + - name: Coverage gate (90% lines; composition root + main.rs + test-only messaging fake excluded) + env: + CARGO_NET_GIT_FETCH_WITH_CLI: "true" + run: > + cargo llvm-cov --locked --features streaming,streaming-file-parquet,streaming-file-avro,scripting-lua + --ignore-filename-regex '(app|main|test_support)\.rs' + --fail-under-lines 90 diff --git a/src/app.rs b/src/app.rs index 2320ef7..8a8a09b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -14,36 +14,49 @@ //! `metric` class (see [`crate::observe`]); //! - an [`EvtEmitter`] for the processor's own `evt` health events; //! - the custom command verbs `flush` / `get-stats` / `pause` / `resume` (registered on -//! `gg.commands()`), which the built-in `ping` / `reload-config` / `get-configuration` complement. +//! `gg.commands()`), which the built-in `ping` / `reload-config` / `get-configuration` complement; +//! - two `scope: "component"` edge-console panel descriptors (`overview`, `routes`) bound to those +//! verbs — an optional enhancement (not a baseline requirement for processors). //! //! ## Self-echo guard (loop safety) //! Under a fleet `ecv1/+/+/+/data/#` input a `local` republish onto the processor's own `data` //! class would be re-consumed → an amplifying loop. The dispatcher restamps `local` output with the -//! processor's identity (see [`crate::proc::route`]) and this fan-out handler drops any inbound -//! message whose `identity` device+component equal the processor's own — so a re-consumed copy is -//! discarded. Cross-device processor chaining still works (a different device does not match). +//! processor's identity (see [`crate::proc::route`]) and the fan-out handler ([`crate::dispatch`]) +//! drops any inbound message whose `identity` device+component equal the processor's own — so a +//! re-consumed copy is discarded. Cross-device processor chaining still works (a different device +//! does not match). +//! +//! ## Why this file is the coverage seam +//! `ProcessorApp::start`/`build_route`/`run` are the composition root: they obtain a live +//! `Config`/`Arc`/`EventsFacade`/`Arc`/`Arc` +//! from a real `&EdgeCommons`, none of which has a public (or test) constructor outside the +//! `edgecommons` crate. That makes this file untestable without a live `EdgeCommons` — there is no +//! broker or protocol involved, just library types this crate cannot fabricate. Every piece of +//! *logic* that does not need `&EdgeCommons` directly (the fan-out handler, the command/panel +//! registration, the pure helpers, script-output-topic validation) lives in [`crate::dispatch`] +//! instead, where it is fully unit-tested. See `AGENTS.md` and the `coverage` job in +//! `.github/workflows/ci.yml`, which excludes only this file and `main.rs`. use std::collections::BTreeMap; -use std::sync::atomic::Ordering; use std::sync::Arc; use edgecommons::config::model::Config; use edgecommons::config::template::resolve; -use edgecommons::messaging::message::{Message, MessageIdentity}; -use edgecommons::messaging::{topic_matches, MessagingService}; +use edgecommons::messaging::message::MessageIdentity; +use edgecommons::messaging::MessagingService; use edgecommons::prelude::*; use edgecommons::uns::reserved_class_of; use rhai::Engine; -use serde_json::{json, Value}; -use tokio::sync::{mpsc, oneshot}; +use tokio::sync::mpsc; use tokio::task::JoinHandle; use crate::config::{ parse_target, GlobalDefaults, RouteConfig, ScriptEngineKind, ScriptStageSpec, StageConfig, }; +use crate::dispatch::{self_subscribe, register_commands, validate_script_output_topic, FilterRoutes, RouteHandle}; use crate::observe::{spawn_metric_emitter, EvtEmitter, RouteStats}; use crate::proc::route::{run_worker, Dispatcher}; -use crate::proc::{now_ms, Control, Pipeline, ProcMsg}; +use crate::proc::{Control, Pipeline, ProcMsg}; /// Default aggregation/partition key when neither the route nor the global defaults set one. const DEFAULT_KEY: &str = "body.signal.id"; @@ -52,10 +65,6 @@ const DEFAULT_QUEUE: usize = 256; /// Depth of a route's out-of-band control channel (the `flush` command verb). const CONTROL_QUEUE: usize = 4; -/// The routes registered on one subscribe filter: each a `(worker data sender, route counters)` -/// pair the fan-out handler forwards to. -type FilterRoutes = Vec<(mpsc::Sender, Arc)>; - /// One fully wired route (produced by [`ProcessorApp::build_route`]). struct BuiltRoute { id: String, @@ -65,15 +74,6 @@ struct BuiltRoute { stats: Arc, } -/// A command-facing route handle. Deliberately holds **no** data sender, so the app remains the -/// sole owner of the data channels — dropping [`ProcessorApp::senders`] on shutdown then closes -/// every worker (the control sender kept here does not gate worker shutdown). -struct RouteHandle { - id: String, - control: mpsc::Sender, - stats: Arc, -} - /// Per-route-invariant build context: the shared Rhai engine, the script-file loader, the /// cross-route defaults, and the component identity injected into scripts. Bundled so `build_route` /// takes the route plus one context, not a long argument list. @@ -208,19 +208,24 @@ impl ProcessorApp { let stats_vec: Vec> = built.iter().map(|b| b.stats.clone()).collect(); app.metric_task = Some(spawn_metric_emitter(gg.metrics(), config.clone(), stats_vec)); - // 4. Register the custom command verbs (flush / get-stats / pause / resume). The built-in - // ping / reload-config / get-configuration are already wired by the library. - let handles: Arc> = Arc::new( - built - .iter() - .map(|b| RouteHandle { - id: b.id.clone(), - control: b.control.clone(), - stats: b.stats.clone(), - }) - .collect(), - ); - register_commands(gg, handles); + // 4. Register the custom command verbs (flush / get-stats / pause / resume) + the console + // panel pair. The built-in ping / reload-config / get-configuration are already wired by + // the library. A no-op when no messaging transport wired an inbox. + if let Some(cmds) = gg.commands() { + let handles: Arc> = Arc::new( + built + .iter() + .map(|b| RouteHandle { + id: b.id.clone(), + control: b.control.clone(), + stats: b.stats.clone(), + }) + .collect(), + ); + register_commands(&cmds, handles); + } else { + tracing::debug!("no command inbox (no messaging transport); custom verbs not registered"); + } tracing::info!( routes = app.workers.len(), @@ -372,254 +377,3 @@ impl ProcessorApp { Ok(()) } } - -/// Subscribe one filter with a fan-out handler: apply the UNS self-echo guard, then forward each -/// message to every route channel that registered for it (tallying `messages_in`/`messages_dropped` -/// and the queue-depth gauge, honoring the per-route `paused` flag, and emitting a rate-limited -/// `evt/warning/queue-overflow` on backpressure drops). Concurrency is 1 so ordered consumers get -/// messages in order. -async fn self_subscribe( - messaging: &Arc, - filter: &str, - routes: FilterRoutes, - own_device: String, - own_component: String, - evt: Arc, -) -> anyhow::Result<()> { - let filter_owned = filter.to_string(); - messaging - .subscribe( - filter, - message_handler(move |topic, msg| { - let routes = routes.clone(); - let filter_owned = filter_owned.clone(); - let own_device = own_device.clone(); - let own_component = own_component.clone(); - let evt = evt.clone(); - async move { - // Self-echo guard: drop our own re-consumed output (identity device+component - // match ours) so a `local` republish onto the consumed `data` class can't loop. - if let Some(id) = &msg.identity { - if id.device() == own_device && id.component() == own_component { - tracing::trace!(topic = %topic, "self-echo dropped (own identity)"); - return; - } - } - let pm = ProcMsg { topic, msg, recv_ms: now_ms() }; - for (tx, stats) in &routes { - if stats.is_paused() { - continue; - } - match tx.try_send(pm.clone()) { - Ok(()) => { - stats.messages_in.fetch_add(1, Ordering::Relaxed); - } - Err(_) => { - stats.messages_dropped.fetch_add(1, Ordering::Relaxed); - tracing::debug!( - filter = %filter_owned, - route = %stats.id, - "route queue full; dropping message" - ); - evt.queue_overflow(&stats.id).await; - } - } - // Update the queue-depth gauge (max_capacity - remaining permits). - let depth = tx.max_capacity().saturating_sub(tx.capacity()) as u64; - stats.queue_depth.store(depth, Ordering::Relaxed); - } - } - }), - DEFAULT_QUEUE, - 1, - ) - .await?; - Ok(()) -} - -/// Register the processor's custom command verbs on the library command inbox (a no-op when no -/// messaging transport wired an inbox). The built-in `ping`/`reload-config`/`get-configuration` -/// verbs are registered by the library and complement these. -fn register_commands(gg: &EdgeCommons, handles: Arc>) { - let Some(cmds) = gg.commands() else { - tracing::debug!("no command inbox (no messaging transport); custom verbs not registered"); - return; - }; - - // get-stats — per-route counters snapshot. - { - let handles = handles.clone(); - try_register(&cmds, "get-stats", command_handler(move |_req| { - let handles = handles.clone(); - async move { Ok(Some(stats_json(&handles))) } - })); - } - - // flush — force-close every route's open time windows now; report the total emitted. - { - let handles = handles.clone(); - try_register(&cmds, "flush", command_handler(move |_req| { - let handles = handles.clone(); - async move { - let mut flushed = 0u64; - for route in handles.iter() { - let (reply_tx, reply_rx) = oneshot::channel(); - if route.control.send(Control::Flush(reply_tx)).await.is_ok() { - if let Ok(n) = reply_rx.await { - flushed += n; - } - } - } - Ok(Some(json!({ "flushed": flushed }))) - } - })); - } - - // pause — stop enqueuing to a route (body `{route}`), or all routes when omitted. - { - let handles = handles.clone(); - try_register(&cmds, "pause", command_handler(move |req| { - let handles = handles.clone(); - async move { Ok(Some(set_paused(&handles, &req, true))) } - })); - } - - // resume — the inverse of pause. - { - let handles = handles.clone(); - try_register(&cmds, "resume", command_handler(move |req| { - let handles = handles.clone(); - async move { Ok(Some(set_paused(&handles, &req, false))) } - })); - } -} - -/// Register a verb, logging (not failing) if the inbox rejects it. -fn try_register(cmds: &Arc, verb: &str, handler: Arc) { - if let Err(e) = cmds.register(verb, handler) { - tracing::warn!(verb, error = %e, "failed to register command verb"); - } -} - -/// The `get-stats` reply body: `{routes: [{id, in, out, dropped, streamAppends, publishFailures, -/// queueDepth, paused}]}`. -fn stats_json(handles: &[RouteHandle]) -> Value { - let routes: Vec = handles - .iter() - .map(|r| { - json!({ - "id": r.id, - "in": r.stats.messages_in.load(Ordering::Relaxed), - "out": r.stats.messages_out.load(Ordering::Relaxed), - "dropped": r.stats.messages_dropped.load(Ordering::Relaxed), - "streamAppends": r.stats.stream_appends.load(Ordering::Relaxed), - "publishFailures": r.stats.publish_failures.load(Ordering::Relaxed), - "queueDepth": r.stats.queue_depth.load(Ordering::Relaxed), - "paused": r.stats.paused.load(Ordering::Relaxed), - }) - }) - .collect(); - json!({ "routes": routes }) -} - -/// Apply `paused` to the route named in `request.body.route` (or all routes when absent). Returns -/// `{paused|resumed: [ids...]}`. -fn set_paused(handles: &[RouteHandle], request: &Message, paused: bool) -> Value { - let route = request.body.get("route").and_then(Value::as_str); - let mut affected = Vec::new(); - for r in handles { - if route.is_none() || route == Some(r.id.as_str()) { - r.stats.paused.store(paused, Ordering::Relaxed); - affected.push(r.id.clone()); - } - } - let key = if paused { "paused" } else { "resumed" }; - json!({ key: affected }) -} - -/// Validate a script stage's resolved `output.topic` for one route: reject reserved UNS classes, -/// reject an output that any of the route's own subscribe filters would re-consume (a direct -/// feedback loop — cross-route re-consumption is stopped at runtime by the self-echo guard), and -/// reject a route-level `publish.topic` alongside it (the dispatcher's per-route topic would -/// silently override the per-stage output topic). -fn validate_script_output_topic( - route_id: &str, - topic: &str, - include_root: bool, - filters: &[String], - route_publish_topic: Option<&str>, -) -> anyhow::Result<()> { - if let Some(cls) = reserved_class_of(topic, include_root) { - anyhow::bail!( - "route '{route_id}': script output.topic '{topic}' targets the RESERVED UNS class \ - '{}' — target a data/evt/app class instead", - cls.token() - ); - } - for f in filters { - anyhow::ensure!( - !topic_matches(f, topic), - "route '{route_id}': script output.topic '{topic}' overlaps this route's subscribe \ - filter '{f}' — a feedback loop; publish the derived signal outside the route's input \ - filters" - ); - } - anyhow::ensure!( - route_publish_topic.is_none(), - "route '{route_id}': `publish.topic` and a script stage `output.topic` are mutually \ - exclusive — the route-level topic would override the stage output; drop one of the two" - ); - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn script_output_topic_validation() { - let filters = vec!["ecv1/+/+/+/data/#".to_string()]; - // A data-class topic under the subscribed fleet filter is a feedback loop. - let err = validate_script_output_topic( - "r1", - "ecv1/gw-1/telemetry-processor/r1/data/derived", - true, - &filters, - None, - ) - .unwrap_err(); - assert!(err.to_string().contains("feedback loop"), "{err}"); - - // A reserved class (`metric`) is rejected outright. - let err = validate_script_output_topic( - "r1", - "ecv1/gw-1/telemetry-processor/r1/metric/derived", - true, - &[], - None, - ) - .unwrap_err(); - assert!(err.to_string().contains("RESERVED"), "{err}"); - - // A route publish.topic alongside a stage output topic is ambiguous. - let err = validate_script_output_topic( - "r1", - "ecv1/gw-1/telemetry-processor/r1/data/derived", - true, - &[], - Some("ecv1/gw-1/telemetry-processor/r1/data/other"), - ) - .unwrap_err(); - assert!(err.to_string().contains("mutually"), "{err}"); - - // A non-overlapping data topic with no publish.topic passes. - validate_script_output_topic( - "r1", - "ecv1/gw-1/telemetry-processor/r1/data/derived", - true, - &["ecv1/+/opcua-adapter/+/data/#".to_string()], - None, - ) - .unwrap(); - } -} diff --git a/src/dispatch.rs b/src/dispatch.rs new file mode 100644 index 0000000..244d184 --- /dev/null +++ b/src/dispatch.rs @@ -0,0 +1,518 @@ +//! # Fan-out, command surface, and edge-console panels +//! +//! Everything in this module is testable **without a live `EdgeCommons`**: it operates over trait +//! objects a downstream test can fake ([`edgecommons::messaging::MessagingService`], the crate's own +//! [`crate::observe::EvtEmitter`]) or is pure. It is split out of [`crate::app`] specifically so the +//! coverage gate can hold this logic to the 90% floor while [`crate::app`]'s composition root +//! (`ProcessorApp::start`/`build_route`/`run`) — which must obtain a live `Config`/`CommandInbox`/ +//! `EventsFacade` from a real `EdgeCommons`, none of which has a public test constructor — is +//! excluded as the genuine untestable seam (see `AGENTS.md` and `.github/workflows/ci.yml`). +//! +//! - [`self_subscribe`] wires one subscribe filter's fan-out handler: the UNS self-echo guard, then +//! per-route delivery honoring `paused` and tallying `messages_in`/`messages_dropped`/`queue_depth`, +//! with a rate-limited `evt/warning/queue-overflow` on backpressure. +//! - [`register_commands`] wires the custom command verbs (`get-stats`/`flush`/`pause`/`resume`) and +//! the two edge-console panel descriptors onto the library's command inbox. +//! - [`validate_script_output_topic`] rejects a script stage's `output.topic` that targets a reserved +//! UNS class, feeds back into the route's own subscribe filters, or collides with a route-level +//! `publish.topic`. + +use std::sync::atomic::Ordering; +use std::sync::Arc; + +use edgecommons::messaging::message::Message; +use edgecommons::messaging::{topic_matches, MessagingService}; +use edgecommons::prelude::*; +use edgecommons::uns::reserved_class_of; +use serde_json::{json, Value}; +use tokio::sync::{mpsc, oneshot}; + +use crate::observe::{EvtEmitter, RouteStats}; +use crate::proc::{now_ms, Control, ProcMsg}; + +/// The routes registered on one subscribe filter: each a `(worker data sender, route counters)` +/// pair the fan-out handler forwards to. +pub(crate) type FilterRoutes = Vec<(mpsc::Sender, Arc)>; + +/// A command-facing route handle. Deliberately holds **no** data sender, so [`crate::app::ProcessorApp`] +/// remains the sole owner of the data channels — dropping its senders on shutdown then closes every +/// worker (the control sender kept here does not gate worker shutdown). +pub(crate) struct RouteHandle { + pub id: String, + pub control: mpsc::Sender, + pub stats: Arc, +} + +/// Subscribe one filter with a fan-out handler: apply the UNS self-echo guard, then forward each +/// message to every route channel that registered for it (tallying `messages_in`/`messages_dropped` +/// and the queue-depth gauge, honoring the per-route `paused` flag, and emitting a rate-limited +/// `evt/warning/queue-overflow` on backpressure drops). Concurrency is 1 so ordered consumers get +/// messages in order. +pub(crate) async fn self_subscribe( + messaging: &Arc, + filter: &str, + routes: FilterRoutes, + own_device: String, + own_component: String, + evt: Arc, +) -> anyhow::Result<()> { + let filter_owned = filter.to_string(); + messaging + .subscribe( + filter, + message_handler(move |topic, msg| { + let routes = routes.clone(); + let filter_owned = filter_owned.clone(); + let own_device = own_device.clone(); + let own_component = own_component.clone(); + let evt = evt.clone(); + async move { + // Self-echo guard: drop our own re-consumed output (identity device+component + // match ours) so a `local` republish onto the consumed `data` class can't loop. + if let Some(id) = &msg.identity { + if id.device() == own_device && id.component() == own_component { + tracing::trace!(topic = %topic, "self-echo dropped (own identity)"); + return; + } + } + let pm = ProcMsg { topic, msg, recv_ms: now_ms() }; + for (tx, stats) in &routes { + if stats.is_paused() { + continue; + } + match tx.try_send(pm.clone()) { + Ok(()) => { + stats.messages_in.fetch_add(1, Ordering::Relaxed); + } + Err(_) => { + stats.messages_dropped.fetch_add(1, Ordering::Relaxed); + tracing::debug!( + filter = %filter_owned, + route = %stats.id, + "route queue full; dropping message" + ); + evt.queue_overflow(&stats.id).await; + } + } + // Update the queue-depth gauge (max_capacity - remaining permits). + let depth = tx.max_capacity().saturating_sub(tx.capacity()) as u64; + stats.queue_depth.store(depth, Ordering::Relaxed); + } + } + }), + 256, + 1, + ) + .await?; + Ok(()) +} + +/// Register the processor's custom command verbs on the library command inbox (a no-op when no +/// messaging transport wired an inbox). The built-in `ping`/`reload-config`/`get-configuration` +/// verbs are registered by the library and complement these. +pub(crate) fn register_commands(cmds: &Arc, handles: Arc>) { + // get-stats — per-route counters snapshot. + { + let handles = handles.clone(); + try_register(cmds, "get-stats", command_handler(move |_req| { + let handles = handles.clone(); + async move { Ok(Some(stats_json(&handles))) } + })); + } + + // flush — force-close every route's open time windows now; report the total emitted. + { + let handles = handles.clone(); + try_register(cmds, "flush", command_handler(move |_req| { + let handles = handles.clone(); + async move { + let mut flushed = 0u64; + for route in handles.iter() { + let (reply_tx, reply_rx) = oneshot::channel(); + if route.control.send(Control::Flush(reply_tx)).await.is_ok() { + if let Ok(n) = reply_rx.await { + flushed += n; + } + } + } + Ok(Some(json!({ "flushed": flushed }))) + } + })); + } + + // pause — stop enqueuing to a route (body `{route}`), or all routes when omitted. + { + let handles = handles.clone(); + try_register(cmds, "pause", command_handler(move |req| { + let handles = handles.clone(); + async move { Ok(Some(set_paused(&handles, &req, true))) } + })); + } + + // resume — the inverse of pause. + { + let handles = handles.clone(); + try_register(cmds, "resume", command_handler(move |req| { + let handles = handles.clone(); + async move { Ok(Some(set_paused(&handles, &req, false))) } + })); + } + + // The edge-console panel pair (optional enhancement, not a baseline requirement for + // processors — see DESIGN.md). Component-scoped: the processor has no console-facing UNS + // instance dimension (routes are internal wiring, addressed by an optional body field, not a + // topic segment), unlike a southbound adapter's per-device instances. + for panel in panels() { + if let Err(e) = cmds.register_panel(panel) { + tracing::warn!(error = %e, "failed to register edge-console panel"); + } + } +} + +/// The two edge-console panel descriptors: `overview` (fleet-wide totals + flush/pause/resume) and +/// `routes` (per-route counters via `get-stats`). Both `scope: "component"` (see [`register_commands`]). +pub(crate) fn panels() -> Vec { + vec![ + json!({ + "id": "overview", "title": "Overview", "order": 10, "scope": "component", + "widgets": [ + { "kind": "commandSummary", "actions": ["flush", "pause", "resume"] } + ], + "verbs": ["get-stats", "flush", "pause", "resume"] + }), + json!({ + "id": "routes", "title": "Routes", "order": 20, "scope": "component", + "widgets": [ { "kind": "table", "source": "get-stats", "path": "routes" } ], + "verbs": ["get-stats"] + }), + ] +} + +/// Register a verb, logging (not failing) if the inbox rejects it. +fn try_register(cmds: &Arc, verb: &str, handler: Arc) { + if let Err(e) = cmds.register(verb, handler) { + tracing::warn!(verb, error = %e, "failed to register command verb"); + } +} + +/// The `get-stats` reply body: `{routes: [{id, in, out, dropped, streamAppends, publishFailures, +/// queueDepth, paused}]}`. +pub(crate) fn stats_json(handles: &[RouteHandle]) -> Value { + let routes: Vec = handles + .iter() + .map(|r| { + json!({ + "id": r.id, + "in": r.stats.messages_in.load(Ordering::Relaxed), + "out": r.stats.messages_out.load(Ordering::Relaxed), + "dropped": r.stats.messages_dropped.load(Ordering::Relaxed), + "streamAppends": r.stats.stream_appends.load(Ordering::Relaxed), + "publishFailures": r.stats.publish_failures.load(Ordering::Relaxed), + "queueDepth": r.stats.queue_depth.load(Ordering::Relaxed), + "paused": r.stats.paused.load(Ordering::Relaxed), + }) + }) + .collect(); + json!({ "routes": routes }) +} + +/// Apply `paused` to the route named in `request.body.route` (or all routes when absent). Returns +/// `{paused|resumed: [ids...]}`. +pub(crate) fn set_paused(handles: &[RouteHandle], request: &Message, paused: bool) -> Value { + let route = request.body.get("route").and_then(Value::as_str); + let mut affected = Vec::new(); + for r in handles { + if route.is_none() || route == Some(r.id.as_str()) { + r.stats.paused.store(paused, Ordering::Relaxed); + affected.push(r.id.clone()); + } + } + let key = if paused { "paused" } else { "resumed" }; + json!({ key: affected }) +} + +/// Validate a script stage's resolved `output.topic` for one route: reject reserved UNS classes, +/// reject an output that any of the route's own subscribe filters would re-consume (a direct +/// feedback loop — cross-route re-consumption is stopped at runtime by the self-echo guard), and +/// reject a route-level `publish.topic` alongside it (the dispatcher's per-route topic would +/// silently override the per-stage output topic). +pub(crate) fn validate_script_output_topic( + route_id: &str, + topic: &str, + include_root: bool, + filters: &[String], + route_publish_topic: Option<&str>, +) -> anyhow::Result<()> { + if let Some(cls) = reserved_class_of(topic, include_root) { + anyhow::bail!( + "route '{route_id}': script output.topic '{topic}' targets the RESERVED UNS class \ + '{}' — target a data/evt/app class instead", + cls.token() + ); + } + for f in filters { + anyhow::ensure!( + !topic_matches(f, topic), + "route '{route_id}': script output.topic '{topic}' overlaps this route's subscribe \ + filter '{f}' — a feedback loop; publish the derived signal outside the route's input \ + filters" + ); + } + anyhow::ensure!( + route_publish_topic.is_none(), + "route '{route_id}': `publish.topic` and a script stage `output.topic` are mutually \ + exclusive — the route-level topic would override the stage output; drop one of the two" + ); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_support::FakeMessaging; + use edgecommons::messaging::message::{HierEntry, MessageBuilder, MessageIdentity}; + + #[test] + fn the_two_panels_are_registered_with_the_right_ids_orders_scope_and_verbs() { + let ps = panels(); + let ids: Vec<&str> = ps.iter().map(|p| p["id"].as_str().unwrap()).collect(); + assert_eq!(ids, vec!["overview", "routes"]); + let orders: Vec = ps.iter().map(|p| p["order"].as_u64().unwrap()).collect(); + assert_eq!(orders, vec![10, 20]); + for p in &ps { + assert_eq!(p["scope"], json!("component"), "the processor has no console instance dimension"); + assert!(!p["title"].as_str().unwrap().is_empty()); + } + assert_eq!(ps[0]["verbs"], json!(["get-stats", "flush", "pause", "resume"])); + assert_eq!(ps[1]["verbs"], json!(["get-stats"])); + } + + #[test] + fn script_output_topic_validation() { + let filters = vec!["ecv1/+/+/+/data/#".to_string()]; + // A data-class topic under the subscribed fleet filter is a feedback loop. + let err = validate_script_output_topic( + "r1", + "ecv1/gw-1/telemetry-processor/r1/data/derived", + true, + &filters, + None, + ) + .unwrap_err(); + assert!(err.to_string().contains("feedback loop"), "{err}"); + + // A reserved class (`metric`) is rejected outright. + let err = validate_script_output_topic( + "r1", + "ecv1/gw-1/telemetry-processor/r1/metric/derived", + true, + &[], + None, + ) + .unwrap_err(); + assert!(err.to_string().contains("RESERVED"), "{err}"); + + // A route publish.topic alongside a stage output topic is ambiguous. + let err = validate_script_output_topic( + "r1", + "ecv1/gw-1/telemetry-processor/r1/data/derived", + true, + &[], + Some("ecv1/gw-1/telemetry-processor/r1/data/other"), + ) + .unwrap_err(); + assert!(err.to_string().contains("mutually"), "{err}"); + + // A non-overlapping data topic with no publish.topic passes. + validate_script_output_topic( + "r1", + "ecv1/gw-1/telemetry-processor/r1/data/derived", + true, + &["ecv1/+/opcua-adapter/+/data/#".to_string()], + None, + ) + .unwrap(); + } + + // ---- stats_json / set_paused (pure) ----------------------------------------------------------- + + fn a_handle(id: &str) -> RouteHandle { + let (control, _rx) = mpsc::channel(1); + RouteHandle { id: id.to_string(), control, stats: RouteStats::new(id) } + } + + #[test] + fn stats_json_reports_every_route_counter() { + let h = a_handle("r1"); + h.stats.messages_in.store(10, Ordering::Relaxed); + h.stats.messages_out.store(7, Ordering::Relaxed); + h.stats.messages_dropped.store(1, Ordering::Relaxed); + h.stats.stream_appends.store(3, Ordering::Relaxed); + h.stats.publish_failures.store(2, Ordering::Relaxed); + h.stats.queue_depth.store(5, Ordering::Relaxed); + h.stats.paused.store(true, Ordering::Relaxed); + + let out = stats_json(&[h]); + let routes = out["routes"].as_array().unwrap(); + assert_eq!(routes.len(), 1); + assert_eq!(routes[0]["id"], json!("r1")); + assert_eq!(routes[0]["in"], json!(10)); + assert_eq!(routes[0]["out"], json!(7)); + assert_eq!(routes[0]["dropped"], json!(1)); + assert_eq!(routes[0]["streamAppends"], json!(3)); + assert_eq!(routes[0]["publishFailures"], json!(2)); + assert_eq!(routes[0]["queueDepth"], json!(5)); + assert_eq!(routes[0]["paused"], json!(true)); + } + + fn a_request(body: Value) -> Message { + MessageBuilder::new("cmd", "1.0").command(body).build() + } + + #[test] + fn set_paused_targets_one_route_when_named_and_all_when_omitted() { + let handles = vec![a_handle("r1"), a_handle("r2")]; + + let out = set_paused(&handles, &a_request(json!({ "route": "r1" })), true); + assert_eq!(out["paused"], json!(["r1"])); + assert!(handles[0].stats.is_paused()); + assert!(!handles[1].stats.is_paused()); + + let out = set_paused(&handles, &a_request(json!({})), true); + assert_eq!(out["paused"].as_array().unwrap().len(), 2, "no route -> every route"); + assert!(handles[1].stats.is_paused()); + + let out = set_paused(&handles, &a_request(json!({})), false); + assert_eq!(out["resumed"].as_array().unwrap().len(), 2); + assert!(!handles[0].stats.is_paused()); + assert!(!handles[1].stats.is_paused()); + } + + // ---- self_subscribe (the fan-out handler) ----------------------------------------------------- + + fn southbound(signal_id: &str) -> Message { + MessageBuilder::new("SouthboundSignalUpdate", "1.0") + .southbound_signal_update(json!({ + "signal": { "id": signal_id, "name": signal_id }, + "samples": [{ "value": 1.0, "quality": "GOOD" }] + })) + .build() + } + + #[tokio::test] + async fn fans_a_message_out_to_every_route_registered_on_the_filter() { + let fake = FakeMessaging::new(); + let messaging: Arc = fake.clone(); + let (_rec, evt) = EvtEmitter::recording(); + let (tx1, mut rx1) = mpsc::channel(4); + let (tx2, mut rx2) = mpsc::channel(4); + let s1 = RouteStats::new("r1"); + let s2 = RouteStats::new("r2"); + + self_subscribe( + &messaging, + "ecv1/+/+/+/data/#", + vec![(tx1, s1.clone()), (tx2, s2.clone())], + "my-device".into(), + "telemetry-processor".into(), + evt, + ) + .await + .unwrap(); + assert!(fake.is_subscribed("ecv1/+/+/+/data/#")); + + fake.deliver("ecv1/+/+/+/data/#", "ecv1/other/opcua-adapter/kep1/data/x", southbound("x")).await; + + assert_eq!(rx1.try_recv().unwrap().topic, "ecv1/other/opcua-adapter/kep1/data/x"); + assert_eq!(rx2.try_recv().unwrap().topic, "ecv1/other/opcua-adapter/kep1/data/x"); + assert_eq!(s1.messages_in.load(Ordering::Relaxed), 1); + assert_eq!(s2.messages_in.load(Ordering::Relaxed), 1); + } + + #[tokio::test] + async fn drops_a_re_consumed_message_carrying_its_own_identity() { + let fake = FakeMessaging::new(); + let messaging: Arc = fake.clone(); + let (_rec, evt) = EvtEmitter::recording(); + let (tx, mut rx) = mpsc::channel(4); + let stats = RouteStats::new("r1"); + + self_subscribe( + &messaging, + "ecv1/+/+/+/data/#", + vec![(tx, stats.clone())], + "my-device".into(), + "telemetry-processor".into(), + evt, + ) + .await + .unwrap(); + + let mut own_echo = southbound("downsampled"); + own_echo.identity = Some( + MessageIdentity::new( + vec![HierEntry { level: "device".into(), value: "my-device".into() }], + "telemetry-processor", + Some("r1".into()), + ) + .unwrap(), + ); + fake.deliver("ecv1/+/+/+/data/#", "ecv1/my-device/telemetry-processor/r1/data/x", own_echo).await; + + assert!(rx.try_recv().is_err(), "the self-echo must be dropped, not forwarded"); + assert_eq!(stats.messages_in.load(Ordering::Relaxed), 0); + } + + #[tokio::test] + async fn a_full_queue_drops_and_counts_and_emits_once() { + let fake = FakeMessaging::new(); + let messaging: Arc = fake.clone(); + let (rec, evt) = EvtEmitter::recording(); + let (tx, _rx) = mpsc::channel(1); // capacity 1 — the second delivery finds it full + let stats = RouteStats::new("r1"); + + self_subscribe( + &messaging, + "ecv1/+/+/+/data/#", + vec![(tx, stats.clone())], + "my-device".into(), + "telemetry-processor".into(), + evt, + ) + .await + .unwrap(); + + // Fill the one-slot queue, then overflow it. + fake.deliver("ecv1/+/+/+/data/#", "t1", southbound("a")).await; + fake.deliver("ecv1/+/+/+/data/#", "t2", southbound("b")).await; + assert_eq!(stats.messages_in.load(Ordering::Relaxed), 1); + assert_eq!(stats.messages_dropped.load(Ordering::Relaxed), 1); + assert_eq!(rec.lock().unwrap().len(), 1, "queue-overflow evt emitted once"); + } + + #[tokio::test] + async fn a_paused_route_is_skipped_entirely() { + let fake = FakeMessaging::new(); + let messaging: Arc = fake.clone(); + let (rec, evt) = EvtEmitter::recording(); + let (tx, _rx) = mpsc::channel(4); + let paused_stats = RouteStats::new("r2"); + paused_stats.paused.store(true, Ordering::Relaxed); + + self_subscribe( + &messaging, + "ecv1/+/opcua-adapter/+/data/#", + vec![(tx, paused_stats.clone())], + "my-device".into(), + "telemetry-processor".into(), + evt, + ) + .await + .unwrap(); + + fake.deliver("ecv1/+/opcua-adapter/+/data/#", "t3", southbound("c")).await; + assert_eq!(paused_stats.messages_in.load(Ordering::Relaxed), 0); + assert_eq!(paused_stats.messages_dropped.load(Ordering::Relaxed), 0); + assert!(rec.lock().unwrap().is_empty(), "a skipped route never overflows"); + } +} diff --git a/src/main.rs b/src/main.rs index 2afd386..7afe1a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,9 +18,12 @@ mod app; mod config; +mod dispatch; mod json_path; mod observe; mod proc; +#[cfg(test)] +mod test_support; use edgecommons::prelude::*; diff --git a/src/observe.rs b/src/observe.rs index 2592232..7032175 100644 --- a/src/observe.rs +++ b/src/observe.rs @@ -94,15 +94,49 @@ impl RouteStats { /// per-channel **cooldown** gate. Publishing is best-effort: `evt` is a non-reserved class, so the /// reserved-class guard passes; a failed publish is logged at DEBUG and swallowed (the facade /// itself never propagates a transport error here — see [`EventsFacade::emit`]). +/// One recorded `(event_type, message, context)` tuple, captured by [`EvtEmitter::recording`]. +#[cfg(test)] +pub(crate) type RecordedEvts = Arc, Value)>>>; + pub struct EvtEmitter { - events: EventsFacade, + /// `None` only in the test-only [`EvtEmitter::recording`] constructor — [`EventsFacade`] has no + /// public constructor outside the `edgecommons` crate (by design), so a unit test that needs to + /// exercise the cooldown gate + call sites (the fan-out handler, the route dispatcher) without a + /// live `EdgeCommons` cannot build a real one. Production code always goes through + /// [`EvtEmitter::new`], which always populates this. + events: Option, cooldowns: Mutex>, + /// Test-only recorder (see [`EvtEmitter::recording`]): when set, [`Self::emit`] records the + /// `(event_type, message, context)` tuple for in-process assertions instead of publishing + /// through a facade. + #[cfg(test)] + recorder: Option, } impl EvtEmitter { /// Build an emitter over the component's `events()` facade (`gg.events()`). pub fn new(events: EventsFacade) -> Arc { - Arc::new(EvtEmitter { events, cooldowns: Mutex::new(HashMap::new()) }) + Arc::new(EvtEmitter { + events: Some(events), + cooldowns: Mutex::new(HashMap::new()), + #[cfg(test)] + recorder: None, + }) + } + + /// A test-only emitter with no bound facade: [`Self::emit`] records into the returned recorder + /// instead of publishing, so component code that depends on an `EvtEmitter` (the fan-out + /// handler, the route [`crate::proc::route::Dispatcher`]) is unit-testable without a live + /// `EdgeCommons`/`EventsFacade`. + #[cfg(test)] + pub(crate) fn recording() -> (RecordedEvts, Arc) { + let recorder = Arc::new(Mutex::new(Vec::new())); + let emitter = Arc::new(EvtEmitter { + events: None, + cooldowns: Mutex::new(HashMap::new()), + recorder: Some(recorder.clone()), + }); + (recorder, emitter) } /// Returns `true` if `event_type` is outside its cooldown (and records the emit time). @@ -125,8 +159,13 @@ impl EvtEmitter { if !self.allow(event_type) { return; } - if let Err(e) = self.events.emit(Severity::Warning, event_type, message, Some(context)).await - { + #[cfg(test)] + if let Some(rec) = &self.recorder { + rec.lock().unwrap().push((event_type.to_string(), message, context)); + return; + } + let Some(events) = &self.events else { return }; + if let Err(e) = events.emit(Severity::Warning, event_type, message, Some(context)).await { tracing::debug!(error = %e, event_type, "evt publish failed"); } } @@ -287,4 +326,32 @@ mod tests { true } } + + #[tokio::test] + async fn recording_emitter_records_each_health_event_with_its_context() { + let (rec, emitter) = EvtEmitter::recording(); + emitter.queue_overflow("r1").await; + emitter.route_error("r1", "ecv1/x/telemetry-processor/data/y", "publish failed").await; + emitter.stream_unavailable("r2", "archive", "sink down").await; + + let recorded = rec.lock().unwrap(); + assert_eq!(recorded.len(), 3); + assert_eq!(recorded[0].0, "queue-overflow"); + assert_eq!(recorded[0].1, None); + assert_eq!(recorded[0].2, json!({ "route": "r1" })); + assert_eq!(recorded[1].0, "route-error"); + assert_eq!(recorded[1].1.as_deref(), Some("publish failed")); + assert_eq!(recorded[1].2, json!({ "route": "r1", "topic": "ecv1/x/telemetry-processor/data/y" })); + assert_eq!(recorded[2].0, "stream-unavailable"); + assert_eq!(recorded[2].1.as_deref(), Some("sink down")); + assert_eq!(recorded[2].2, json!({ "route": "r2", "stream": "archive" })); + } + + #[tokio::test] + async fn recording_emitter_is_also_cooldown_gated() { + let (rec, emitter) = EvtEmitter::recording(); + emitter.queue_overflow("r1").await; + emitter.queue_overflow("r1").await; // within the cooldown window — suppressed + assert_eq!(rec.lock().unwrap().len(), 1); + } } diff --git a/src/proc/route.rs b/src/proc/route.rs index 83379fc..85b9855 100644 --- a/src/proc/route.rs +++ b/src/proc/route.rs @@ -249,7 +249,10 @@ pub async fn run_worker( #[cfg(test)] mod tests { - use edgecommons::messaging::message::{Message, MessageBuilder}; + use super::*; + use crate::observe::EvtEmitter; + use crate::test_support::FakeMessaging; + use edgecommons::messaging::message::MessageBuilder; use serde_json::json; #[test] @@ -275,4 +278,172 @@ mod tests { assert_eq!(decoded.body["samples"][0]["sourceTsMs"], json!(1783360799900u64)); assert_eq!(decoded.body["samples"][0]["serverTsMs"], json!(1783360800000u64)); } + + fn southbound(topic: &str) -> ProcMsg { + let msg = MessageBuilder::new("SouthboundSignalUpdate", "1.0") + .southbound_signal_update(json!({ + "signal": { "id": "temp", "name": "Temperature" }, + "samples": [{ "value": 21.5, "quality": "GOOD" }] + })) + .build(); + ProcMsg { topic: topic.to_string(), msg, recv_ms: 1_000 } + } + + fn publish_cfg(topic: Option<&str>, qos: Option<&str>, partition_key: Option<&str>) -> PublishConfig { + PublishConfig { + topic: topic.map(String::from), + partition_key: partition_key.map(String::from), + qos: qos.map(String::from), + } + } + + #[allow(clippy::too_many_arguments)] + fn dispatcher( + messaging: Arc, + target: Channel, + publish: &PublishConfig, + stats: Arc, + evt: Arc, + restamp: Option, + ) -> Dispatcher { + Dispatcher::new( + messaging, + target, + publish, + "body.signal.id", + "r1".to_string(), + stats, + evt, + restamp, + #[cfg(feature = "streaming")] + None, + ) + } + + #[tokio::test] + async fn local_dispatch_uses_the_source_topic_by_default_and_tallies_out() { + let fake = FakeMessaging::new(); + let messaging: Arc = fake.clone(); + let stats = RouteStats::new("r1"); + let (_rec, evt) = EvtEmitter::recording(); + let d = dispatcher(messaging, Channel::Local, &publish_cfg(None, None, None), stats.clone(), evt, None); + + d.dispatch(southbound("ecv1/gw/opcua-adapter/kep1/data/temp")).await; + + let published = fake.published.lock().unwrap(); + assert_eq!(published.len(), 1); + assert_eq!(published[0].0, "ecv1/gw/opcua-adapter/kep1/data/temp", "default topic = source topic"); + assert_eq!(stats.messages_out.load(Ordering::Relaxed), 1); + } + + #[tokio::test] + async fn local_dispatch_restamps_identity_when_configured() { + let fake = FakeMessaging::new(); + let messaging: Arc = fake.clone(); + let stats = RouteStats::new("r1"); + let (_rec, evt) = EvtEmitter::recording(); + let restamp = MessageIdentity::new( + vec![edgecommons::messaging::message::HierEntry { + level: "device".into(), + value: "gw".into(), + }], + "telemetry-processor", + Some("r1".into()), + ) + .unwrap(); + let d = dispatcher( + messaging, + Channel::Local, + &publish_cfg(Some("ecv1/{ThingName}/telemetry-processor/data/downsampled"), None, None), + stats, + evt, + Some(restamp.clone()), + ); + + d.dispatch(southbound("ecv1/gw/opcua-adapter/kep1/data/temp")).await; + + let published = fake.published.lock().unwrap(); + assert_eq!(published[0].0, "ecv1/{ThingName}/telemetry-processor/data/downsampled"); + assert_eq!(published[0].1.identity.as_ref().unwrap(), &restamp, "output carries the processor's identity"); + } + + #[tokio::test] + async fn a_failed_local_publish_is_tallied_and_emits_route_error() { + let fake = FakeMessaging::new(); + fake.set_fail_publish("broker unavailable"); + let messaging: Arc = fake.clone(); + let stats = RouteStats::new("r1"); + let (rec, evt) = EvtEmitter::recording(); + let d = dispatcher(messaging, Channel::Local, &publish_cfg(None, None, None), stats.clone(), evt, None); + + d.dispatch(southbound("ecv1/gw/opcua-adapter/kep1/data/temp")).await; + + assert_eq!(stats.publish_failures.load(Ordering::Relaxed), 1); + assert_eq!(stats.messages_out.load(Ordering::Relaxed), 0); + let recorded = rec.lock().unwrap(); + assert_eq!(recorded[0].0, "route-error"); + } + + #[tokio::test] + async fn northbound_dispatch_uses_the_configured_qos() { + let fake = FakeMessaging::new(); + let messaging: Arc = fake.clone(); + let stats = RouteStats::new("r1"); + let (_rec, evt) = EvtEmitter::recording(); + let d = dispatcher( + messaging, + Channel::Northbound, + &publish_cfg(Some("ecv1/{ThingName}/telemetry-processor/evt/alarms"), Some("atMostOnce"), None), + stats.clone(), + evt, + None, + ); + + d.dispatch(southbound("ecv1/gw/opcua-adapter/kep1/data/temp")).await; + + let nb = fake.northbound.lock().unwrap(); + assert_eq!(nb.len(), 1); + assert_eq!(nb[0].0, "ecv1/{ThingName}/telemetry-processor/evt/alarms"); + assert_eq!(nb[0].2, Qos::AtMostOnce); + assert_eq!(stats.messages_out.load(Ordering::Relaxed), 1); + } + + #[tokio::test] + async fn a_failed_northbound_publish_is_tallied_and_emits_route_error() { + let fake = FakeMessaging::new(); + fake.set_fail_publish("northbound link down"); + let messaging: Arc = fake.clone(); + let stats = RouteStats::new("r1"); + let (rec, evt) = EvtEmitter::recording(); + let d = + dispatcher(messaging, Channel::Northbound, &publish_cfg(None, None, None), stats.clone(), evt, None); + + d.dispatch(southbound("ecv1/gw/opcua-adapter/kep1/data/temp")).await; + + assert_eq!(stats.publish_failures.load(Ordering::Relaxed), 1); + assert_eq!(rec.lock().unwrap()[0].0, "route-error"); + } + + #[tokio::test] + async fn a_stream_target_with_no_configured_handle_is_dropped_and_emits_stream_unavailable() { + let fake = FakeMessaging::new(); + let messaging: Arc = fake.clone(); + let stats = RouteStats::new("r1"); + let (rec, evt) = EvtEmitter::recording(); + let d = dispatcher( + messaging, + Channel::Stream("archive".to_string()), + &publish_cfg(None, None, Some("body.signal.id")), + stats.clone(), + evt, + None, + ); + + d.dispatch(southbound("ecv1/gw/opcua-adapter/kep1/data/temp")).await; + + assert_eq!(stats.publish_failures.load(Ordering::Relaxed), 1); + assert_eq!(stats.stream_appends.load(Ordering::Relaxed), 0); + assert_eq!(rec.lock().unwrap()[0].0, "stream-unavailable"); + assert!(fake.published.lock().unwrap().is_empty(), "a stream target never touches messaging"); + } } diff --git a/src/test_support.rs b/src/test_support.rs new file mode 100644 index 0000000..4b3c347 --- /dev/null +++ b/src/test_support.rs @@ -0,0 +1,166 @@ +//! # Test-only fakes shared across unit tests (compiled only under `#[cfg(test)]`) +//! +//! [`FakeMessaging`] implements [`MessagingService`] entirely in-process: it records every +//! `publish`/`publish_northbound` call and lets a test invoke a subscribed handler directly +//! ([`FakeMessaging::deliver`]) to simulate an inbound message — no broker required. This is the +//! downstream analog of the (crate-private) `edgecommons::testutil::RecordingMessaging` the library +//! uses for its own tests; component crates cannot reach that type (it is `pub(crate)`), so this +//! crate carries its own, exactly as `file-replicator`'s `src/events.rs` carries its own recording +//! substitute for the (also unconstructable outside the library) `EventsFacade` (see +//! [`crate::observe::EvtEmitter::recording`]). + +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use edgecommons::messaging::message::Message; +use edgecommons::prelude::{MessageHandler, MessagingService, Qos, ReplyFuture}; +use edgecommons::{EdgeCommonsError, Result}; + +/// A [`MessagingService`] fake: records local/northbound publishes, and lets a test simulate an +/// inbound message by invoking the handler stored for a subscribed filter. +#[derive(Default)] +pub(crate) struct FakeMessaging { + pub published: Mutex>, + pub northbound: Mutex>, + handlers: Mutex>>, + pub unsubscribed: Mutex>, + /// When set, `publish`/`publish_northbound` return this error instead of recording — drives the + /// "a publish failure is tallied + surfaced as `evt`" tests. + fail_publish: Mutex>, +} + +impl FakeMessaging { + pub fn new() -> Arc { + Arc::new(Self::default()) + } + + /// Simulate an inbound message on `filter`'s handler (a no-op if nothing subscribed it). + pub async fn deliver(&self, filter: &str, topic: &str, msg: Message) { + let handler = self.handlers.lock().unwrap().get(filter).cloned(); + if let Some(h) = handler { + h.handle(topic.to_string(), msg).await; + } + } + + /// Whether `filter` currently has a live subscription (inserted by `subscribe`, removed by + /// `unsubscribe`). + pub fn is_subscribed(&self, filter: &str) -> bool { + self.handlers.lock().unwrap().contains_key(filter) + } + + pub fn set_fail_publish(&self, reason: impl Into) { + *self.fail_publish.lock().unwrap() = Some(reason.into()); + } +} + +#[async_trait::async_trait] +impl MessagingService for FakeMessaging { + async fn publish(&self, topic: &str, msg: &Message) -> Result<()> { + if let Some(reason) = self.fail_publish.lock().unwrap().clone() { + return Err(EdgeCommonsError::Messaging(reason)); + } + self.published.lock().unwrap().push((topic.to_string(), msg.clone())); + Ok(()) + } + + async fn publish_northbound(&self, topic: &str, msg: &Message, qos: Qos) -> Result<()> { + if let Some(reason) = self.fail_publish.lock().unwrap().clone() { + return Err(EdgeCommonsError::Messaging(reason)); + } + self.northbound.lock().unwrap().push((topic.to_string(), msg.clone(), qos)); + Ok(()) + } + + async fn publish_raw(&self, _topic: &str, _payload: &serde_json::Value) -> Result<()> { + Err(EdgeCommonsError::Messaging("publish_raw not supported by FakeMessaging".into())) + } + + async fn publish_northbound_raw( + &self, + _topic: &str, + _payload: &serde_json::Value, + _qos: Qos, + ) -> Result<()> { + Err(EdgeCommonsError::Messaging( + "publish_northbound_raw not supported by FakeMessaging".into(), + )) + } + + async fn subscribe( + &self, + filter: &str, + handler: Arc, + _max_messages: usize, + _max_concurrency: usize, + ) -> Result<()> { + self.handlers.lock().unwrap().insert(filter.to_string(), handler); + Ok(()) + } + + async fn subscribe_northbound( + &self, + _filter: &str, + _handler: Arc, + _qos: Qos, + _max_messages: usize, + _max_concurrency: usize, + ) -> Result<()> { + Err(EdgeCommonsError::Messaging("subscribe_northbound not supported by FakeMessaging".into())) + } + + async fn unsubscribe(&self, filter: &str) -> Result<()> { + self.handlers.lock().unwrap().remove(filter); + self.unsubscribed.lock().unwrap().push(filter.to_string()); + Ok(()) + } + + async fn unsubscribe_northbound(&self, _filter: &str) -> Result<()> { + Ok(()) + } + + async fn request(&self, _topic: &str, _msg: Message) -> Result { + Err(EdgeCommonsError::Messaging("request not supported by FakeMessaging".into())) + } + + async fn request_northbound(&self, _topic: &str, _msg: Message) -> Result { + Err(EdgeCommonsError::Messaging("request_northbound not supported by FakeMessaging".into())) + } + + async fn request_with_timeout( + &self, + _topic: &str, + _msg: Message, + _timeout: Option, + ) -> Result { + Err(EdgeCommonsError::Messaging( + "request_with_timeout not supported by FakeMessaging".into(), + )) + } + + async fn request_northbound_with_timeout( + &self, + _topic: &str, + _msg: Message, + _timeout: Option, + ) -> Result { + Err(EdgeCommonsError::Messaging( + "request_northbound_with_timeout not supported by FakeMessaging".into(), + )) + } + + async fn reply(&self, _request: &Message, _reply: Message) -> Result<()> { + Err(EdgeCommonsError::Messaging("reply not supported by FakeMessaging".into())) + } + + async fn reply_northbound(&self, _request: &Message, _reply: Message) -> Result<()> { + Err(EdgeCommonsError::Messaging("reply_northbound not supported by FakeMessaging".into())) + } + + fn cancel_request(&self, _reply_future: ReplyFuture) {} + fn cancel_request_northbound(&self, _reply_future: ReplyFuture) {} + + fn connected(&self) -> bool { + true + } +} From e5b8c92e0f0a0990b3ff5f521797587a75ec7c63 Mon Sep 17 00:00:00 2001 From: breis Date: Sun, 19 Jul 2026 22:19:43 -0400 Subject: [PATCH 4/7] ci: add deploy-docs.yml so docs-only pushes refresh the docs site ci.yml's paths-ignore (**.md, docs/**) means a docs-only push triggers no workflow at all today, so the docs site never refreshes until an unrelated code push happens to land alongside it. Adds the hook-guarded docs-rebuild trigger (verbatim the modbus-adapter shape): hits the Cloudflare Workers Builds deploy hook for edgecommons-docs on a docs-only push to main, a no-op until the repo's CLOUDFLARE_DEPLOY_HOOK secret is set. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01DxV5wFi7LCratBERVT6aiL --- .github/workflows/deploy-docs.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/deploy-docs.yml diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000..4f44451 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,25 @@ +name: deploy-docs + +# Doc-only pushes skip the build CI (see paths-ignore in ci.yml) but should still refresh the +# EdgeCommons docs site, which aggregates this repo's docs/ at build time. This hits the Cloudflare +# Workers Builds deploy hook for the edgecommons-docs project to trigger a rebuild (a few seconds of +# Actions, not the full build). Enable by setting the repo Actions secret CLOUDFLARE_DEPLOY_HOOK to +# the deploy-hook URL; without it this is a no-op (docs rebuild on the next edgecommons main push). +on: + push: + branches: [main] + paths: ['docs/**', '**.md'] + +jobs: + trigger-docs-rebuild: + runs-on: ubuntu-latest + steps: + - name: Trigger Cloudflare docs rebuild + env: + HOOK: ${{ secrets.CLOUDFLARE_DEPLOY_HOOK }} + run: | + if [ -n "$HOOK" ]; then + curl -fsS -X POST "$HOOK" && echo "triggered edgecommons-docs rebuild" + else + echo "CLOUDFLARE_DEPLOY_HOOK not set; skipping (docs rebuild on the next edgecommons main push)" + fi From 0034fc4a98be29ee0a36a5bf16af69912e0a111c Mon Sep 17 00:00:00 2001 From: breis Date: Sun, 19 Jul 2026 22:19:55 -0400 Subject: [PATCH 5/7] feat: add config.schema.json for component.global + component.instances[] `edgecommons component validate` had nothing to check this component's config against: no schema file existed anywhere in the repo, so a typo'd pipeline key was caught by no tool at any stage. Models component.global.defaults (the block edgecommons component validate currently checks) plus $defs.route/$defs.stage describing each component.instances[] entry -- the full route/pipeline-stage surface (filter, sample, aggregate, project, script incl. the multi-signal inputs/output form) -- for documentation and forward compatibility (the CLI does not yet validate instances[] against a component schema; see ec-validate's schema.rs). Deliberately does not redeclare the library envelope (logging, messaging, metricEmission, heartbeat, streaming, tags, component.token, ...), which the canonical schema owns. Validated against both shipped test-configs/ samples (component.global + every instances[] route) with a real JSON Schema validator (ajv, draft 2020-12): both pass; a typo'd key in either section is rejected. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01DxV5wFi7LCratBERVT6aiL --- config.schema.json | 315 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 config.schema.json diff --git a/config.schema.json b/config.schema.json new file mode 100644 index 0000000..d37ecac --- /dev/null +++ b/config.schema.json @@ -0,0 +1,315 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://docs.edgecommons.mbreissi.com/schemas/telemetry-processor/config.schema.json", + "title": "telemetry-processor component configuration", + "description": "The configuration telemetry-processor itself understands: the object at `component.global` (checked here by `edgecommons component validate`), plus reusable `$defs` describing each `component.instances[]` entry (one ROUTE per instance — see `$defs/route`) for documentation and forward compatibility. The library envelope (logging, messaging, metricEmission, heartbeat, streaming, credentials, parameters, health, hierarchy, identity, tags, `component.token`) is owned by the canonical schema and is deliberately not redeclared here. `additionalProperties: false` throughout so a typo'd key is caught rather than silently ignored. See docs/reference/configuration.md for the authoritative field-by-field prose this schema was derived from.", + "type": "object", + "additionalProperties": false, + "properties": { + "defaults": { + "type": "object", + "additionalProperties": false, + "description": "Cross-route defaults under `component.global.defaults`, overlaid by each route (global ⊕ instance, instance wins per key).", + "properties": { + "key": { + "type": "string", + "default": "body.signal.id", + "description": "Default aggregation / sample / stream-partition key path (a dotted key path — see `$defs/keyPath`)." + }, + "target": { + "type": "string", + "description": "Default `target` for a route that omits one: `local` | `northbound` | `stream:`." + }, + "scriptsDir": { + "type": "string", + "description": "Base directory for `{\"file\": \"...\"}` script references (template-resolved at startup). A relative script path resolves against it; an absolute path is used as-is. Defaults to the process working directory when unset." + }, + "scriptEngine": { + "$ref": "#/$defs/scriptEngine", + "default": "rhai", + "description": "Default engine for every route's `filter`/`script` stages; a route's own `scriptEngine` overrides this." + } + } + } + }, + "$defs": { + "scriptEngine": { + "type": "string", + "enum": ["rhai", "lua"], + "description": "`rhai` (pure-Rust, always compiled in) or `lua` (Lua 5.4 — needs the component's `scripting-lua` build feature)." + }, + "keyPath": { + "type": "string", + "minLength": 1, + "description": "A dotted path over the message: roots `body.` (default when no known root prefix is present), `identity.`, `tags.`, `header.`; a `[]` suffix on a segment spreads across an array. Examples: `body.signal.id`, `body.samples[].quality`, `identity.device`." + }, + "route": { + "title": "One route — a component.instances[] entry", + "description": "Each `component.instances[]` entry is one independent route: subscribe → pipeline → target. A malformed instance is logged and skipped at startup; startup fails only if zero routes build.", + "type": "object", + "additionalProperties": false, + "required": ["id", "subscribe"], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "description": "Route id — log-only identity, and (for `local` targets) the UNS `{instance}` token the dispatcher restamps output with. Unique among the component's routes." + }, + "subscribe": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 }, + "description": "Topic filters on the local bus (MQTT `+`/`#` wildcards allowed). Each filter is template-resolved at startup. A filter shared by several routes is subscribed once and fanned out.", + "examples": [["ecv1/+/+/+/data/#"], ["ecv1/+/opcua-adapter/+/data/#"]] + }, + "pipeline": { + "type": "array", + "default": [], + "description": "Ordered processing stages. Empty = pass-through republisher.", + "items": { "$ref": "#/$defs/stage" } + }, + "target": { + "type": "string", + "description": "Where the route's output goes. Required unless `global.defaults.target` is set. `local` (republish on the local bus), `northbound` (IoT Core / a northbound MQTT broker), or `stream:` (a named entry under the top-level `streaming.streams[]`).", + "examples": ["local", "northbound", "stream:archive"] + }, + "publish": { "$ref": "#/$defs/publish" }, + "key": { + "$ref": "#/$defs/keyPath", + "description": "Route default key path for `sample`/`aggregate`/stream partitioning. Falls back to `global.defaults.key`, then `body.signal.id`." + }, + "maxQueue": { + "type": "integer", + "minimum": 1, + "default": 256, + "description": "Per-route internal queue depth (also the broker-side subscribe queue depth). Drop-on-full: when the route's worker can't keep up, new messages are dropped (tallied in `get-stats` `dropped`, surfaced as a rate-limited `evt/warning/queue-overflow`). Numeric fields accept an integer or an integer-valued float (Greengrass delivers config numbers as doubles)." + }, + "scriptEngine": { + "$ref": "#/$defs/scriptEngine", + "description": "Engine for this route's `filter`/`script` stages. Falls back to `global.defaults.scriptEngine`, then `rhai`." + } + } + }, + "publish": { + "title": "instances[].publish", + "type": "object", + "additionalProperties": false, + "properties": { + "topic": { + "type": "string", + "description": "Output topic template for `local`/`northbound` (default: the source topic). Template-resolved at startup. Must not resolve to a reserved UNS class (`state`/`metric`/`cfg`/`log`) and must not be combined with a script stage's own `output.topic`." + }, + "partitionKey": { + "$ref": "#/$defs/keyPath", + "description": "Partition-key path for `stream:` targets (default: the route `key`, resolved per message)." + }, + "qos": { + "type": "string", + "enum": ["atLeastOnce", "atMostOnce"], + "default": "atLeastOnce", + "description": "`northbound` targets only." + } + } + }, + "stage": { + "title": "One pipeline stage", + "description": "An externally-tagged object: exactly one of `filter` / `sample` / `aggregate` / `project` / `script`.", + "type": "object", + "minProperties": 1, + "maxProperties": 1, + "properties": { + "filter": { "$ref": "#/$defs/filterStage" }, + "sample": { "$ref": "#/$defs/sampleStage" }, + "aggregate": { "$ref": "#/$defs/aggregateStage" }, + "project": { "$ref": "#/$defs/projectStage" }, + "script": { "$ref": "#/$defs/scriptStage" } + }, + "additionalProperties": false + }, + "filterStage": { + "title": "filter — keep/drop whole messages", + "description": "Exactly one form applies, checked in order: `script` → `quality` → `field`. A filter with none of the three fails the route at build time.", + "type": "object", + "additionalProperties": false, + "properties": { + "script": { + "$ref": "#/$defs/scriptSource", + "description": "A boolean predicate (per the route's `scriptEngine`) over the message view; keep the message when it returns `true`." + }, + "quality": { + "type": "string", + "description": "Shorthand: keep only when every `body.samples[].quality` equals this (and ≥1 sample exists).", + "examples": ["GOOD"] + }, + "field": { + "$ref": "#/$defs/keyPath", + "description": "Built-in predicate path (supports a `[]` array-spread segment → any-element match)." + }, + "op": { + "type": "string", + "enum": ["eq", "ne", "gt", "lt", "ge", "le", "exists", "contains", "==", "!=", ">", "<", ">=", "<="], + "default": "eq", + "description": "Comparison operator for the `field` form." + }, + "value": { + "description": "Right-hand value for the `field`/`op` comparison. Numbers and numeric strings compare numerically." + } + } + }, + "sampleStage": { + "title": "sample — per-key downsampling", + "description": "Needs exactly one of `everyMs` / `everyN`.", + "type": "object", + "additionalProperties": false, + "properties": { + "everyMs": { + "type": "integer", + "minimum": 1, + "description": "Keep at most one message per key per this many ms." + }, + "everyN": { + "type": "integer", + "minimum": 1, + "description": "Keep one in every N messages per key." + }, + "by": { + "$ref": "#/$defs/keyPath", + "description": "Per-key path. Falls back to the route `key`." + } + } + }, + "aggregateStage": { + "title": "aggregate — tumbling windowed reduction", + "description": "Emits one ProcessedTelemetry message per (key, window) on close. Time windows close on the worker flush tick or when a message for a newer window arrives; count windows close in-line when N is reached.", + "type": "object", + "additionalProperties": false, + "required": ["window", "fn"], + "properties": { + "window": { + "type": "string", + "description": "A time window (`\"10s\"` / `\"500ms\"`), or a bare record count (`\"100\"`).", + "examples": ["10s", "500ms", "100"] + }, + "by": { + "$ref": "#/$defs/keyPath", + "description": "Per-key path. Falls back to the route `key`." + }, + "fn": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "enum": ["avg", "max", "min", "sum", "count", "first", "last"] }, + "description": "Reducers. The first listed is the primary (lands in `samples[0].value`)." + }, + "value": { + "$ref": "#/$defs/keyPath", + "description": "Path to the value(s) to fold (a `[]` segment spreads an array). Defaults to every `body.samples[].value`, falling back to the whole body for a payload with no `samples`. Set this for a non-SouthboundSignalUpdate payload, e.g. `body.temperature`." + } + } + }, + "projectStage": { + "title": "project — reshape / whitelist the body", + "description": "With neither `keep` nor `set`, the body passes through unchanged.", + "type": "object", + "additionalProperties": false, + "properties": { + "keep": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "description": "Body paths to retain. Only the first path segment (top-level body key) is kept — e.g. `\"signal.id\"` retains the whole `signal` object." + }, + "set": { + "type": "object", + "description": "Literal fields overlaid onto the body." + } + } + }, + "scriptStage": { + "title": "script — a transform (Rhai or Lua)", + "description": "Inline source (`{\"script\": \"\"}`), the object form (`{\"script\": {\"file\"|\"source\": ..., \"inputs\": {...}, \"output\": {...}}}`, exactly one of file/source required), or the multi-signal object form carrying stateful `inputs` and/or an explicit `output`. All forms compile once at startup.", + "oneOf": [ + { "type": "string", "minLength": 1, "description": "Inline source. Good for a one-liner." }, + { "$ref": "#/$defs/scriptSpec" } + ] + }, + "scriptSource": { + "description": "Inline script source, or `{\"file\": \"\"}` read once at startup (relative to `global.defaults.scriptsDir`, or absolute).", + "oneOf": [ + { "type": "string", "minLength": 1 }, + { + "type": "object", + "additionalProperties": false, + "required": ["file"], + "properties": { "file": { "type": "string", "minLength": 1 } } + } + ] + }, + "scriptSpec": { + "title": "The object form of a script stage", + "type": "object", + "additionalProperties": false, + "properties": { + "file": { + "type": "string", + "minLength": 1, + "description": "Path to a `.rhai`/`.lua` file (relative to `global.defaults.scriptsDir`, or absolute), read once at startup. Exactly one of `file`/`source` is required." + }, + "source": { + "type": "string", + "minLength": 1, + "description": "Inline script source (the object-form equivalent of the bare string form). Exactly one of `file`/`source` is required." + }, + "inputs": { + "type": "object", + "description": "Named multi-signal inputs: `{name: selector}`. Turns this into the stateful multi-signal stage — the stage caches the latest observation of every input and evaluates the script when a matched input's value or quality changes.", + "additionalProperties": { "$ref": "#/$defs/inputSelector" } + }, + "output": { + "$ref": "#/$defs/outputSpec", + "description": "Publish each successful evaluation as a new EdgeCommons envelope on `output.topic` instead of mutating the triggering message in place." + } + } + }, + "inputSelector": { + "title": "A named script input's selector", + "description": "At least one of `signalId`/`signalName`/`topic` is required; unknown selector fields fail the route at build time.", + "type": "object", + "additionalProperties": false, + "properties": { + "signalId": { "type": "string", "description": "Match `body.signal.id`." }, + "signalName": { "type": "string", "description": "Match `body.signal.name`." }, + "topic": { "type": "string", "description": "Match the arriving topic against this MQTT-style filter (`+`/`#` supported)." }, + "device": { "type": "string", "description": "Match the source envelope identity's device." }, + "component": { "type": "string", "description": "Match the source envelope identity's component token." }, + "instance": { "type": "string", "description": "Match the source envelope identity's instance token." }, + "required": { + "type": "boolean", + "default": false, + "description": "When `true`, the stage withholds every evaluation until this input has been observed. When `false` (default), the stage never waits on it." + } + } + }, + "outputSpec": { + "title": "An explicit script output", + "type": "object", + "additionalProperties": false, + "required": ["topic"], + "properties": { + "topic": { + "type": "string", + "minLength": 1, + "description": "The output topic (template-resolved at startup). Must not be a reserved UNS class and must not match one of the route's own `subscribe` filters (a feedback loop)." + }, + "name": { + "type": "string", + "default": "ScriptResult", + "description": "The envelope header `name` of the derived message." + }, + "version": { + "type": "string", + "default": "1.0", + "description": "The envelope header `version` of the derived message." + } + } + } + } +} From 5870c8b42832d8df594f547e51e263f47986864d Mon Sep 17 00:00:00 2001 From: breis Date: Sun, 19 Jul 2026 22:20:08 -0400 Subject: [PATCH 6/7] docs: add AGENTS.md, CLAUDE.md, and DESIGN.md governance files The design substance already existed (rich module-level doc headers, the monorepo's processor design docs) but the repo carried no agent-facing shape/conventions file and no in-repo design register. AGENTS.md: the processor's shape (the Processor stage seam, the app.rs/dispatch.rs testability split, config location, validation expectations) plus the org conventions it inherits. CLAUDE.md: a short header + `@AGENTS.md` import + the local-dev .cargo override note, per the org scaffold shape. DESIGN.md: a decision register (D-TP-1..8) seeded from the shipped v2 design (payload-agnostic core, external script files + the multi-signal script stage, tags-as-JSON-column) plus this remediation's own decisions (the committed lockfile, the license fix, the coverage-seam split, the console panels), the command/metrics surface, and validation evidence. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01DxV5wFi7LCratBERVT6aiL --- AGENTS.md | 93 ++++++++++++++++++++++++++++++++++++ CLAUDE.md | 20 ++++++++ DESIGN.md | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 252 insertions(+) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 DESIGN.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b88aae6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,93 @@ +# telemetry-processor — component notes + +EdgeCommons **processing component** (Rust). Full name `com.mbreissi.edgecommons.TelemetryProcessor`, +crate/binary `telemetry-processor`. Depends on the `edgecommons` Rust library. If this repo lives +inside the EdgeCommons org umbrella workspace, read its root `AGENTS.md` first (org repo map, +design-fidelity contract, validation matrix, platform/transport model); everything below is this +component's own detail. + +## What it is + +The high-throughput northbound seam between southbound protocol adapters (which publish +`SouthboundSignalUpdate` telemetry on the local bus) and the cloud. It **subscribes** to configured +local topics, runs a declarative per-route **pipeline** — `filter` / `sample` / `aggregate` / +`project` / `script` (Rhai or Lua) — and **forwards** the result to `local`, `northbound`, or a +durable `stream:` (Kinesis / Kafka / rolling Parquet-AVRO files). Runs on `GREENGRASS` / `HOST` +/ `KUBERNETES` via `edgecommons` — no platform branching in this component's own code. + +## The seam + +`src/proc/mod.rs`'s `Processor` trait is the one place stage logic lives: `process` handles an +inbound message and returns zero or more; `on_tick` lets a stateful stage (an aggregate window) emit +on a timer instead of on arrival. Everything above it — `src/app.rs` (the composition root: reads +config, builds routes, spawns workers), `src/dispatch.rs` (the self-echo guard + fan-out handler, +command-verb + console-panel registration), and `src/proc/route.rs` (the per-route worker + target +dispatch) — is written against the trait and does not change when a new stage is added. +`src/proc/script.rs` implements the `script`/`filter.script` stage over either engine (Rhai always +compiled in; Lua behind the `scripting-lua` feature) and its stateful multi-signal variant +(`src/proc/multi.rs`). `src/app.rs` and `src/dispatch.rs` split along a testability line, not a +logical one — see [Validation expectations](#validation-expectations). + +## Config location + +This component's own settings live under `component.global` (cross-route defaults) / +`component.instances[]` (one **route** per instance) in the EdgeCommons config document — +`config.schema.json` is the contract `edgecommons component validate` checks `component.global` +against, and its `$defs.route`/`$defs.stage` describe each instance for documentation and forward +compatibility. The sibling sections (`tags`, `messaging`, `streaming`, `logging`, `heartbeat`, +`metricEmission`) are the standard `edgecommons` envelope, owned by the canonical schema and not +redeclared here. `test-configs/` carries two runnable examples (`config.json`, +`standalone-messaging.json`). See `docs/reference/configuration.md` for the full field-by-field +reference. + +## Validation expectations + +- `cargo test` covers the pipeline stages (`src/proc/*.rs`), route config (`src/config.rs`), the + route dispatcher (`src/proc/route.rs`), the fan-out handler + command/panel registration + (`src/dispatch.rs`), and the metric/event surface (`src/observe.rs`) directly — no broker required. +- `cargo llvm-cov --fail-under-lines 90` is the coverage gate (`.github/workflows/ci.yml`'s + `coverage` job) — the org rule is 90% line coverage per language. This repo has no live-infra + *driver* seam of its own (the Kinesis/Kafka/file-sink clients live in the + `edgecommons`/`edgestreamlog` library, not here); the seam it does have is narrower and different + in kind — the **EdgeCommons composition root**. `src/app.rs`'s `ProcessorApp::start`/`build_route`/ + `run` must obtain a live `Config`/`Arc`/`EventsFacade`/`Arc`/ + `Arc` from a real `&EdgeCommons`, and none of those types has a public or test + constructor outside the `edgecommons` crate — there is no way to fabricate one in a unit test. That + is why the wiring logic is split: everything that does *not* need a live `EdgeCommons` directly + (the self-echo guard + fan-out handler, the command verbs, the two console panels, script-output + topic validation) lives in `src/dispatch.rs`, fully unit-tested against a downstream + `MessagingService` fake (`src/test_support.rs`, this crate's own analog of the library's + crate-private `testutil::RecordingMessaging`) and a test-only recording `EvtEmitter` + (`EvtEmitter::recording`, mirroring `file-replicator`'s `Events::recording_events` — `EventsFacade` + likewise has no public constructor). The coverage job excludes exactly three files: + `main.rs` (the runtime bootstrap shim), `app.rs` (the composition root above), and + `test_support.rs` (test-only support code, not production logic — the same treatment + `file-replicator` gives its own `testutil.rs`). Every other line — pipeline mechanics, route/config + parsing, the fan-out handler, dispatch/restamping, the command surface, the metric/event emitters — + stays in the denominator and is unit-tested. Do not lower the gate or exclude testable code to pass + it — add tests. +- The `scripting-lua` feature (Lua 5.4 via vendored `mlua`) is built and tested in CI alongside the + default Rhai-only build, so both script engines are exercised. +- `edgecommons component validate` checks this repo's config against `config.schema.json` and warns + if `Cargo.lock` is not committed. + +## Org conventions this scaffold inherits + +- A processor is **payload-agnostic**: it uses raw `messaging()`/`streams()`, never the `data()` + facade (which mints its own topic from a signal id under its own bound identity — the wrong tool + for republishing an already-built, possibly non-southbound-shaped message). `evt` health events do + use the library's `events()` facade, where its identity/topic/body ownership is the right fit. +- Self-echo guard + identity restamp are load-bearing, not optional style: because the processor + **consumes** the `data` class it also republishes onto (for `local` targets), the dispatcher + restamps `local` output with the processor's own identity and the subscribe fan-out drops any + inbound message whose identity matches that — without both halves, a `local` route would loop. +- A full route queue drops and counts; it never blocks the transport's dispatch task. +- Four-way parity: if this repo's Java/Python/TypeScript siblings exist, observable behavior should + match — same config shape, same metric names, same command verbs. +- Builders/facades are the construction path (`messaging()`, `streams()`, `events()`, `commands()`, + `MetricBuilder`) — never hand-built topics or envelopes. +- Runtime artifacts (vaults, parameter caches, generated streams, TLS certs, logs, build output, + local broker state) stay out of Git. +- `Cargo.lock` is committed (SD-B, org-level lockfile-commit policy): regenerate it with the local + `.cargo/config.toml` `[patch]` override **inactive** so it records the pinned git dependency, not a + local path — a lock recorded against the path override does not resolve on a fresh clone or in CI. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..79097d6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,20 @@ +# telemetry-processor (Claude Code) + +EdgeCommons processing component (Rust), `com.mbreissi.edgecommons.TelemetryProcessor`. The full +picture — what this component is, the stage seam, config location, and the org conventions it +inherits — lives in `AGENTS.md` and is shared with every agent tool. It is imported here in full: + +@AGENTS.md + +## Local-dev notes + +- **Default (committed pin):** `Cargo.toml`'s `edgecommons` dependency is a git `rev` pin, and + `Cargo.lock` is committed against that pin — a plain `cargo build`/`clone` needs only read access + to `edgecommons/edgecommons` (private; the fetch goes through the git CLI, and CI rewrites the URL + with the `EDGECOMMONS_READ_TOKEN` PAT). +- **Building against an unpushed sibling change:** add a gitignored `.cargo/config.toml` next to this + file with a `[patch]` block pointing the git URL at your local `../core/libs/rust` checkout (see the + comment above the `edgecommons` dependency in `Cargo.toml` for the exact form). This is local-dev + only — CI never sees it, and it does not touch the committed `Cargo.lock`/pin. **Do not commit a + `Cargo.lock` regenerated while that override is active** — it would record a local path, not the + git pin, and would not resolve on a fresh clone or in CI. diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..e5b32a7 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,139 @@ +# DESIGN — telemetry-processor + +> Treat this document as the **design-fidelity contract** for this component: before changing +> behavior, update the relevant section here in the same change, and review new work against what +> is written here — not against a summary of it. + +## What it is + +`com.mbreissi.edgecommons.TelemetryProcessor` is the reference Rust **processing** component: it +subscribes to configured local topics, runs a declarative per-route pipeline of stages +(`filter` / `sample` / `aggregate` / `project` / `script`), and forwards each result to `local`, +`northbound`, or a durable `stream:` (Kinesis / Kafka / rolling Parquet-AVRO files). It is the +high-throughput northbound seam between southbound protocol adapters and the cloud. + +## Decisions + +Numbered `D-TP-` so later sessions can cite them. + +- **D-TP-1. Payload-agnostic core.** The processor does not require the `SouthboundSignalUpdate` + shape. Any JSON body matching a route's `subscribe` filter flows through; the southbound shape is + only a *convention* the built-in conveniences default to (the `quality` filter shorthand, the + default key `body.signal.id`, `body.samples[]` aggregation, the file sink's default rows + projection). Each is overridable per route. This is why the dispatcher uses raw + `messaging()`/`streams()` rather than the `data()` facade, which mints its own topic under its own + bound identity and forces a `SouthboundSignalUpdate` shape — wrong for a processor that republishes + an already-built message that may not be southbound-shaped at all (see + `docs/reference/messaging-interface.md`'s "Why this dispatch uses `messaging()`/`streams()`"). +- **D-TP-2. External script files + the multi-signal `script` stage.** A `script`/`filter.script` + stage source is inline or `{"file": "path"}` (resolved against `global.defaults.scriptsDir`), + compiled once at startup. The object form additionally supports named, stateful `inputs` (a + selector per input; the stage caches the latest observation of each and evaluates on any matched + input's value/quality change, binding the full snapshot as `inputs` and the firing input as + `trigger`) and an explicit `output` (publishes a new envelope on its own topic instead of mutating + the triggering message in place, correlated back via `header.correlation_id`). This shipped to let + a single script combine several southbound signals (e.g. an OEE calculation) without a bespoke + join stage. Cached input state is partitioned by source device so two devices publishing the same + signal ids never mix into one snapshot. +- **D-TP-3. Envelope `tags` as a JSON column, not exploded.** The default `rows`-mode file + projection lands the envelope's `tags{}` map as one JSON column rather than one column per tag key + — tags are an open, per-deployment set, so a fixed schema cannot enumerate them, and a JSON column + keeps the Parquet/Avro schema stable across configs. A declared `rows` user projection can still + pull an individual tag value out via its own `path`. +- **D-TP-4. Dispatch bypasses the `data()` facade (see D-TP-1).** `data()` always mints the topic + from its own bound instance identity (breaking the documented `local`/`northbound` "default = the + source topic" bridge), forces the `SouthboundSignalUpdate` header/body shape (which would break + `ProcessedTelemetry`/`project`-reshaped bodies), and always stamps its own identity — right for + `local` (which the self-echo guard depends on) but wrong for `northbound`/`stream`, which must + preserve the source identity for provenance. +- **D-TP-5. Self-echo guard is two mechanisms, not one.** (a) A `local` republish restamps the output + envelope's identity to the processor's own (instance = the route id); (b) the fan-out handler drops + any inbound message whose identity device+component match the processor's own. Removing either + breaks the guard: without (a) a re-consumed `local` output would still carry the *source's* + identity and slip past (b); without (b) restamping alone does nothing to stop the loop. +- **D-TP-6 (this remediation, `#11`/`#9`). `Cargo.lock` is committed (org policy SD-B).** The + `/Cargo.lock` `.gitignore` entry and its now-stale rationale ("excluded until the pin is bumped to + the UNS-core rev") were removed; the lockfile was regenerated with the local `.cargo/config.toml` + `[patch]` override **inactive**, so it records the pinned git dependency + (`edgecommons` rev `36a70c48…`) rather than a local path — valid on a fresh clone and in CI. See + `CLAUDE.md` for the local-dev override that must stay inactive when regenerating this file. + License metadata was also reconciled: `Cargo.toml`'s `license` field was `Apache-2.0` (a stale + leftover) against the actual `BUSL-1.1` `LICENSE` file; fixed to `license = "BUSL-1.1"` (`#9`). +- **D-TP-7 (this remediation). `app.rs` / `dispatch.rs` split is a testability boundary, not a + logical one.** `ProcessorApp::start`/`build_route`/`run` (`src/app.rs`) must obtain a live + `Config`/`Arc`/`EventsFacade`/`Arc`/`Arc` + from a real `&EdgeCommons`; none of those types has a public or test constructor outside the + `edgecommons` crate (`EventsFacade::new` and `CommandInbox::new` are both `pub(crate)` in the + library), so this file cannot be unit-tested without a live runtime — the CI coverage job excludes + it (with `main.rs`) for exactly this reason, mirroring the "supervisor.rs" seam pattern the + protocol-adapter templates use for a genuinely different reason (a live device driver). Everything + that does *not* need `&EdgeCommons` directly — the self-echo guard + fan-out handler, the + `get-stats`/`flush`/`pause`/`resume` command verbs, the two console panels, script-output-topic + validation — was moved to `src/dispatch.rs`, which *is* fully unit-tested: it operates over the + public `MessagingService` trait (faked downstream in `src/test_support.rs`, since the library's own + `testutil::RecordingMessaging` is `pub(crate)` and unreachable from this crate) and a test-only + recording `EvtEmitter` (`EvtEmitter::recording`, added to `src/observe.rs` for the same reason — + `EventsFacade` has no public constructor — mirroring `file-replicator`'s `Events::recording_events` + precedent for the identical constraint). `src/test_support.rs` itself is excluded from the coverage + denominator (test-only support code, not product logic — the same treatment `file-replicator` gives + `testutil.rs`). +- **D-TP-8 (this remediation). Console panels are an enhancement, implemented.** Issue `#11`'s P2-6 + flagged panels as optional for processors (not a baseline requirement). Two `scope: "component"` + panels (`overview`: fleet totals + flush/pause/resume; `routes`: per-route counters via + `get-stats`) were straightforward to add via the library's existing `commands.register_panel` (used + identically by the protocol-adapter templates) and ride the command surface that already shipped — + so they were implemented rather than deferred. `scope: "component"`, not `"instance"`: the + processor has no console-facing UNS instance dimension (a route is internal wiring, addressed by an + optional `body.route` field on the command verbs, not a topic segment), unlike a southbound + adapter's per-device instances. + +## Config + +`config.schema.json` is the source of truth for `component.global`'s shape (checked by +`edgecommons component validate`); its `$defs.route`/`$defs.stage` describe each +`component.instances[]` entry for documentation and forward compatibility (the CLI tool does not yet +validate `instances[]` against a component schema — see `ec-validate`'s `schema.rs` — so these +`$defs` are not yet enforced, only documented). `docs/reference/configuration.md` is the authoritative +field-by-field prose it was derived from; treat that page, not this one, as the shape reference. + +## Command surface + +Beyond the library's automatic `ping` / `reload-config` / `get-configuration`: + +| Verb | Body | Result | +|------|------|--------| +| `get-stats` | — | Per-route counters (`in`/`out`/`dropped`/`streamAppends`/`publishFailures`/`queueDepth`/`paused`). | +| `flush` | — | Force-closes every route's open **time** windows now; `{flushed: n}`. Count windows are unaffected. | +| `pause` | `{route?}` | Stops enqueuing to a route (or all routes when omitted); `{paused: [ids]}`. | +| `resume` | `{route?}` | The inverse of `pause`; `{resumed: [ids]}`. | + +Two edge-console panels (`overview`, `routes`; see D-TP-8) bind to these verbs. Full wire contract in +`docs/reference/messaging-interface.md`. + +## Metrics + +`metric/pipeline` (`messagesIn`/`messagesOut`/`messagesDropped`/`streamAppends`/`publishFailures`), +summed across routes and emitted as interval deltas every 30s via `gg.metrics()`. Per-route detail is +`get-stats`, not a metric dimension. See `docs/reference/metrics.md`. + +## Validation + +- `cargo test` (96 tests as of this remediation) — pipeline mechanics, route config parsing, the + fan-out handler + command/panel registration, the route dispatcher (local/northbound/stream targets, + restamp, failure→evt), the metric/event surface. No broker required. +- `cargo llvm-cov --fail-under-lines 90` — the coverage gate; see D-TP-7 for exactly what is excluded + and why. Measured at ~92.5% lines on this remediation's changeset (native Windows; CI runs on + `ubuntu-latest`). +- `cargo clippy --all-targets -- -D warnings` — clean. +- Live-infra validation (HOST/dual-MQTT, GG-lab, Kubernetes) per the org validation matrix is not + re-run by this remediation — it is a hygiene/CI-shaped change (lockfile, coverage gate, schema, + governance docs, license, a testability-only source split, and two additive console panels), not a + behavioral or wire-contract change. The `test-configs/` samples validate cleanly against + `config.schema.json` (see the PR description for the validation transcript). + +## Open items + +- `edgecommons component validate` currently checks only `component.global` against a component's + schema, not `component.instances[]`/`$defs.route` (see `ec-validate`'s `schema.rs` module doc) — + this is a CLI-side gap, not specific to this repo; the `$defs` in `config.schema.json` are ready for + when that lands. From d94e9fcc7dc3fbef26074d3ac79e5438b2292ee0 Mon Sep 17 00:00:00 2001 From: breis Date: Mon, 20 Jul 2026 07:37:59 -0400 Subject: [PATCH 7/7] fix(coverage): narrow the app.rs exclusion to the genuine live-EdgeCommons seam Adversarial review on PR #12 correctly flagged the app.rs coverage exclusion as broader than a thin live-runtime seam: build_route still contained testable decision branches (target/filter/publish-topic resolution, script-output-topic validation, the restamp policy) that need only a plain Config, not a live EdgeCommons. Moved every such decision into a new src/route_build.rs: resolve_global_wiring (cross-route defaulting + the processor's own identity strings -- previously inlined in ProcessorApp::start), resolve_target, resolve_filters, resolve_publish_topic, resolve_script_output_topics, and compute_restamp. All of it is unit-tested directly against Config::from_value-built configs -- no fake needed, since Config itself has a public test constructor (11 new tests, ~99% line coverage on the new file). What remains in src/app.rs is now only the code that must obtain a live Arc/EventsFacade/Arc/Arc/Arc (gg.streams(), stream targets only -- build_route's one remaining live dependency) or gg.shutdown_signal() from a real &EdgeCommons -- none of those types has a public or test constructor outside the edgecommons crate, so this residual is a genuine, irreducible seam, not an over-broad exclusion. ci.yml's exclusion list is unchanged ((app|main|test_support)\.rs) -- route_build.rs is deliberately NOT excluded, since it is now fully tested and belongs in the denominator. Updated AGENTS.md/DESIGN.md (D-TP-7) to describe the narrower, corrected boundary. Re-validated: cargo test 107/107 (was 96), cargo clippy -D warnings clean, cargo llvm-cov --fail-under-lines 90 -> 92.95% lines (was 92.48%), identical on native Windows and WSL/Linux. Cargo.lock untouched (verified via git diff before and after -- still git-sourced at the pinned rev; all cargo invocations for this change ran in isolated copies outside the worktree, so the ancestor .cargo/config.toml [patch] override never touched the committed lock). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01DxV5wFi7LCratBERVT6aiL --- .github/workflows/ci.yml | 27 +-- AGENTS.md | 68 ++++---- DESIGN.md | 45 +++-- src/app.rs | 149 +++++------------ src/main.rs | 1 + src/route_build.rs | 349 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 476 insertions(+), 163 deletions(-) create mode 100644 src/route_build.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a024b4f..3691121 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,18 +26,21 @@ jobs: # The 90% line-coverage gate (org validation matrix / AGENTS.md), actually RUN here (the # reusable component-ci only does build/test/clippy). This repo has no live-infra driver seam of # its own — the Kinesis/Kafka/file-sink clients live in the `edgecommons`/`edgestreamlog` - # library, not here — so the genuine untestable surface is narrower than a southbound adapter's: - # only `main.rs` (the runtime bootstrap shim) and `app.rs` (`ProcessorApp::start`/`build_route`/ - # `run`, the composition root that must obtain a live `Config`/`Arc`/ - # `EventsFacade`/`Arc`/`Arc` from a real `EdgeCommons` — none of - # which has a public or test constructor outside the `edgecommons` crate) are excluded, each - # pinned to its reason in `src/app.rs`'s module doc and `AGENTS.md`. `test_support.rs` (this - # crate's own downstream `MessagingService` fake, the analog of the library's crate-private - # `testutil::RecordingMessaging`) is test-only support code, not production logic, and is - # excluded the same way `file-replicator` excludes its `testutil.rs`. Every piece of logic that - # does NOT need a live `EdgeCommons` directly — the fan-out handler, the command/panel - # registration, the route dispatcher, the pipeline stages, config parsing — is unit-tested and - # stays in the denominator. Do not lower the gate or exclude testable code to pass it — add tests. + # library, not here — so the excluded set is deliberately THIN, not a broad "the whole wiring + # file" carve-out: `main.rs` (the runtime bootstrap shim) and `app.rs` (only the residual glue + # that obtains a live `Arc`/`EventsFacade`/`Arc`/ + # `Arc`/`Arc` from a real `EdgeCommons` and + # `gg.shutdown_signal()` — none of which has a public or test constructor outside the + # `edgecommons` crate). Every *decision* branch that does not itself require a live + # `EdgeCommons` — cross-route defaulting, route target/filter/publish-topic resolution, + # script-output-topic validation, the `local`-target restamp policy (all of it needs only a + # plain `Config`, built in tests via `Config::from_value`) — was extracted to + # `src/route_build.rs`, which stays in the denominator and is unit-tested (see its own module + # doc + `src/app.rs`'s "Why this file is the coverage seam" doc for the exact boundary). + # `test_support.rs` (this crate's own downstream `MessagingService` fake, the analog of the + # library's crate-private `testutil::RecordingMessaging`) is test-only support code, not + # production logic, and is excluded the same way `file-replicator` excludes its `testutil.rs`. + # Do not lower the gate or exclude testable code to pass it — add tests. runs-on: ubuntu-latest permissions: contents: read diff --git a/AGENTS.md b/AGENTS.md index b88aae6..1fe3d09 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,14 +19,16 @@ durable `stream:` (Kinesis / Kafka / rolling Parquet-AVRO files). Runs on `src/proc/mod.rs`'s `Processor` trait is the one place stage logic lives: `process` handles an inbound message and returns zero or more; `on_tick` lets a stateful stage (an aggregate window) emit -on a timer instead of on arrival. Everything above it — `src/app.rs` (the composition root: reads -config, builds routes, spawns workers), `src/dispatch.rs` (the self-echo guard + fan-out handler, -command-verb + console-panel registration), and `src/proc/route.rs` (the per-route worker + target -dispatch) — is written against the trait and does not change when a new stage is added. -`src/proc/script.rs` implements the `script`/`filter.script` stage over either engine (Rhai always -compiled in; Lua behind the `scripting-lua` feature) and its stateful multi-signal variant -(`src/proc/multi.rs`). `src/app.rs` and `src/dispatch.rs` split along a testability line, not a -logical one — see [Validation expectations](#validation-expectations). +on a timer instead of on arrival. Everything above it — `src/app.rs` (the thin composition root: +obtains live handles from `EdgeCommons`, spawns workers), `src/route_build.rs` (route target/filter/ +publish/script-output-topic resolution, the restamp policy, cross-route defaulting), `src/dispatch.rs` +(the self-echo guard + fan-out handler, command-verb + console-panel registration), and +`src/proc/route.rs` (the per-route worker + target dispatch) — is written against the trait and does +not change when a new stage is added. `src/proc/script.rs` implements the `script`/`filter.script` +stage over either engine (Rhai always compiled in; Lua behind the `scripting-lua` feature) and its +stateful multi-signal variant (`src/proc/multi.rs`). `src/app.rs` vs. `src/route_build.rs`/ +`src/dispatch.rs` split along a testability line, not a logical one — see +[Validation expectations](#validation-expectations). ## Config location @@ -42,30 +44,40 @@ reference. ## Validation expectations -- `cargo test` covers the pipeline stages (`src/proc/*.rs`), route config (`src/config.rs`), the - route dispatcher (`src/proc/route.rs`), the fan-out handler + command/panel registration - (`src/dispatch.rs`), and the metric/event surface (`src/observe.rs`) directly — no broker required. +- `cargo test` covers the pipeline stages (`src/proc/*.rs`), route config (`src/config.rs`), route + build decisions (`src/route_build.rs`), the route dispatcher (`src/proc/route.rs`), the fan-out + handler + command/panel registration (`src/dispatch.rs`), and the metric/event surface + (`src/observe.rs`) directly — no broker required. - `cargo llvm-cov --fail-under-lines 90` is the coverage gate (`.github/workflows/ci.yml`'s `coverage` job) — the org rule is 90% line coverage per language. This repo has no live-infra *driver* seam of its own (the Kinesis/Kafka/file-sink clients live in the `edgecommons`/`edgestreamlog` library, not here); the seam it does have is narrower and different - in kind — the **EdgeCommons composition root**. `src/app.rs`'s `ProcessorApp::start`/`build_route`/ - `run` must obtain a live `Config`/`Arc`/`EventsFacade`/`Arc`/ - `Arc` from a real `&EdgeCommons`, and none of those types has a public or test - constructor outside the `edgecommons` crate — there is no way to fabricate one in a unit test. That - is why the wiring logic is split: everything that does *not* need a live `EdgeCommons` directly - (the self-echo guard + fan-out handler, the command verbs, the two console panels, script-output - topic validation) lives in `src/dispatch.rs`, fully unit-tested against a downstream - `MessagingService` fake (`src/test_support.rs`, this crate's own analog of the library's - crate-private `testutil::RecordingMessaging`) and a test-only recording `EvtEmitter` - (`EvtEmitter::recording`, mirroring `file-replicator`'s `Events::recording_events` — `EventsFacade` - likewise has no public constructor). The coverage job excludes exactly three files: - `main.rs` (the runtime bootstrap shim), `app.rs` (the composition root above), and - `test_support.rs` (test-only support code, not production logic — the same treatment - `file-replicator` gives its own `testutil.rs`). Every other line — pipeline mechanics, route/config - parsing, the fan-out handler, dispatch/restamping, the command surface, the metric/event emitters — - stays in the denominator and is unit-tested. Do not lower the gate or exclude testable code to pass - it — add tests. + in kind — the **EdgeCommons composition root** — and it is kept **thin on purpose**: only the code + that must obtain a live `Arc` (`gg.messaging()`), `EventsFacade` + (`gg.events()`), `Arc` (`gg.commands()`), `Arc` (`gg.metrics()`), + `Arc` (`gg.streams()`, stream targets only), or `gg.shutdown_signal()` stays in + `src/app.rs` — none of those types has a public or test constructor outside the `edgecommons` + crate, so there is no way to fabricate one. Every *decision* that does not itself need a live + `EdgeCommons` was pulled out so it stays in the coverage denominator: + - `src/route_build.rs` — cross-route defaulting (`resolve_global_wiring`), a route's target/ + filter/publish-topic resolution, script-output-topic validation, and the `local`-target restamp + policy. All of it needs only a plain `Config`, which a test builds directly via + `Config::from_value` (no live `EdgeCommons` required) — see its own module doc for the one + exception (`gg.streams()`, which stays in `app.rs`'s `build_route`). + - `src/dispatch.rs` — the self-echo guard + fan-out handler, the command verbs, the two console + panels — unit-tested against a downstream `MessagingService` fake (`src/test_support.rs`, this + crate's own analog of the library's crate-private `testutil::RecordingMessaging`) and a test-only + recording `EvtEmitter` (`EvtEmitter::recording`, mirroring `file-replicator`'s + `Events::recording_events` — `EventsFacade` likewise has no public constructor). + + The coverage job excludes exactly three files: `main.rs` (the runtime bootstrap shim), `app.rs` + (the thin composition root above), and `test_support.rs` (test-only support code, not production + logic — the same treatment `file-replicator` gives its own `testutil.rs`). Every other line — + pipeline mechanics, route/config parsing, route-build decisions, the fan-out handler, dispatch/ + restamping, the command surface, the metric/event emitters — stays in the denominator and is + unit-tested. Do not lower the gate or exclude testable code to pass it — add tests. If you add a + branch to `app.rs` and it doesn't need a live `EdgeCommons` type to run, it almost certainly + belongs in `route_build.rs` or `dispatch.rs` instead. - The `scripting-lua` feature (Lua 5.4 via vendored `mlua`) is built and tested in CI alongside the default Rhai-only build, so both script engines are exercised. - `edgecommons component validate` checks this repo's config against `config.schema.json` and warns diff --git a/DESIGN.md b/DESIGN.md index e5b32a7..cf1118e 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -59,18 +59,27 @@ Numbered `D-TP-` so later sessions can cite them. `CLAUDE.md` for the local-dev override that must stay inactive when regenerating this file. License metadata was also reconciled: `Cargo.toml`'s `license` field was `Apache-2.0` (a stale leftover) against the actual `BUSL-1.1` `LICENSE` file; fixed to `license = "BUSL-1.1"` (`#9`). -- **D-TP-7 (this remediation). `app.rs` / `dispatch.rs` split is a testability boundary, not a - logical one.** `ProcessorApp::start`/`build_route`/`run` (`src/app.rs`) must obtain a live - `Config`/`Arc`/`EventsFacade`/`Arc`/`Arc` - from a real `&EdgeCommons`; none of those types has a public or test constructor outside the - `edgecommons` crate (`EventsFacade::new` and `CommandInbox::new` are both `pub(crate)` in the - library), so this file cannot be unit-tested without a live runtime — the CI coverage job excludes - it (with `main.rs`) for exactly this reason, mirroring the "supervisor.rs" seam pattern the - protocol-adapter templates use for a genuinely different reason (a live device driver). Everything - that does *not* need `&EdgeCommons` directly — the self-echo guard + fan-out handler, the - `get-stats`/`flush`/`pause`/`resume` command verbs, the two console panels, script-output-topic - validation — was moved to `src/dispatch.rs`, which *is* fully unit-tested: it operates over the - public `MessagingService` trait (faked downstream in `src/test_support.rs`, since the library's own +- **D-TP-7 (this remediation, revised). `app.rs` vs. `route_build.rs`/`dispatch.rs` is a testability + boundary, kept deliberately thin, not a broad "the whole wiring file" carve-out.** The first cut of + this split (excluding all of `app.rs`) was too broad — `build_route` still contained testable + decision branches (target/filter/publish-topic resolution, script-output-topic validation, the + restamp policy), all of which need only a plain `Config` (publicly constructible via + `Config::from_value`), not a live `EdgeCommons`. Those moved to a new `src/route_build.rs` + (`resolve_global_wiring`, `resolve_target`, `resolve_filters`, `resolve_publish_topic`, + `resolve_script_output_topics`, `compute_restamp`), unit-tested directly against + `Config::from_value`-built configs — no fakes needed, since `Config` itself has a public test + constructor. What remains in `src/app.rs` is only the code that must obtain a live + `Arc` (`gg.messaging()`), `EventsFacade` (`gg.events()`), + `Arc` (`gg.commands()`), `Arc` (`gg.metrics()`), + `Arc` (`gg.streams()`, stream targets only — `build_route`'s one remaining + live dependency), or `gg.shutdown_signal()` — none of those types has a public or test constructor + outside the `edgecommons` crate (`EventsFacade::new` and `CommandInbox::new` are both `pub(crate)` + in the library), so `app.rs` genuinely cannot be unit-tested without a live runtime. The CI + coverage job excludes it (with `main.rs`) for exactly this reason, mirroring the "supervisor.rs" + seam pattern the protocol-adapter templates use for a genuinely different reason (a live device + driver). The self-echo guard + fan-out handler, the `get-stats`/`flush`/`pause`/`resume` command + verbs, and the two console panels live in `src/dispatch.rs`, unit-tested against a downstream + `MessagingService` fake (`src/test_support.rs`, since the library's own `testutil::RecordingMessaging` is `pub(crate)` and unreachable from this crate) and a test-only recording `EvtEmitter` (`EvtEmitter::recording`, added to `src/observe.rs` for the same reason — `EventsFacade` has no public constructor — mirroring `file-replicator`'s `Events::recording_events` @@ -118,12 +127,14 @@ summed across routes and emitted as interval deltas every 30s via `gg.metrics()` ## Validation -- `cargo test` (96 tests as of this remediation) — pipeline mechanics, route config parsing, the - fan-out handler + command/panel registration, the route dispatcher (local/northbound/stream targets, - restamp, failure→evt), the metric/event surface. No broker required. +- `cargo test` (107 tests as of this remediation) — pipeline mechanics, route config parsing, + route-build decisions (target/filter/publish/script-output-topic resolution, the restamp policy — + `src/route_build.rs`), the fan-out handler + command/panel registration, the route dispatcher + (local/northbound/stream targets, restamp, failure→evt), the metric/event surface. No broker + required. - `cargo llvm-cov --fail-under-lines 90` — the coverage gate; see D-TP-7 for exactly what is excluded - and why. Measured at ~92.5% lines on this remediation's changeset (native Windows; CI runs on - `ubuntu-latest`). + and why. Measured at 92.95% lines on this remediation's changeset, identical on native Windows and + WSL/Linux (CI runs on `ubuntu-latest`). - `cargo clippy --all-targets -- -D warnings` — clean. - Live-infra validation (HOST/dual-MQTT, GG-lab, Kubernetes) per the org validation matrix is not re-run by this remediation — it is a hygiene/CI-shaped change (lockfile, coverage gate, schema, diff --git a/src/app.rs b/src/app.rs index 8a8a09b..6bea25c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -26,40 +26,39 @@ //! re-consumed copy is discarded. Cross-device processor chaining still works (a different device //! does not match). //! -//! ## Why this file is the coverage seam -//! `ProcessorApp::start`/`build_route`/`run` are the composition root: they obtain a live -//! `Config`/`Arc`/`EventsFacade`/`Arc`/`Arc` -//! from a real `&EdgeCommons`, none of which has a public (or test) constructor outside the -//! `edgecommons` crate. That makes this file untestable without a live `EdgeCommons` — there is no -//! broker or protocol involved, just library types this crate cannot fabricate. Every piece of -//! *logic* that does not need `&EdgeCommons` directly (the fan-out handler, the command/panel -//! registration, the pure helpers, script-output-topic validation) lives in [`crate::dispatch`] -//! instead, where it is fully unit-tested. See `AGENTS.md` and the `coverage` job in +//! ## Why this file is the coverage seam — and why it is now narrow +//! The genuinely untestable surface is obtaining live library handles from a real `&EdgeCommons` — +//! `Arc` (`gg.messaging()`), `EventsFacade` (`gg.events()`), +//! `Arc` (`gg.commands()`), `Arc` (`gg.metrics()`), `Arc` (`gg.streams()`, stream targets only), and `gg.shutdown_signal()` — none of which +//! has a public (or test) constructor outside the `edgecommons` crate. That is the only reason this +//! file cannot be unit-tested; there is no broker or protocol involved, just library types this +//! crate cannot fabricate. Every *decision* — cross-route defaulting, route target/filter/publish +//! resolution, script-output-topic validation, the `local`-target restamp policy, the fan-out +//! handler, the command/panel registration — needs only `Config` (publicly constructible via +//! `Config::from_value`) or a fakeable trait object, and lives in [`crate::route_build`] / +//! [`crate::dispatch`] instead, fully unit-tested. See `AGENTS.md` and the `coverage` job in //! `.github/workflows/ci.yml`, which excludes only this file and `main.rs`. use std::collections::BTreeMap; use std::sync::Arc; -use edgecommons::config::model::Config; -use edgecommons::config::template::resolve; -use edgecommons::messaging::message::MessageIdentity; use edgecommons::messaging::MessagingService; use edgecommons::prelude::*; -use edgecommons::uns::reserved_class_of; use rhai::Engine; use tokio::sync::mpsc; use tokio::task::JoinHandle; -use crate::config::{ - parse_target, GlobalDefaults, RouteConfig, ScriptEngineKind, ScriptStageSpec, StageConfig, -}; -use crate::dispatch::{self_subscribe, register_commands, validate_script_output_topic, FilterRoutes, RouteHandle}; +use crate::config::{RouteConfig, ScriptEngineKind}; +use crate::dispatch::{self_subscribe, register_commands, FilterRoutes, RouteHandle}; use crate::observe::{spawn_metric_emitter, EvtEmitter, RouteStats}; use crate::proc::route::{run_worker, Dispatcher}; use crate::proc::{Control, Pipeline, ProcMsg}; +use crate::route_build::{ + compute_restamp, resolve_filters, resolve_global_wiring, resolve_publish_topic, + resolve_script_output_topics, resolve_target, +}; -/// Default aggregation/partition key when neither the route nor the global defaults set one. -const DEFAULT_KEY: &str = "body.signal.id"; /// Default route channel capacity (also the broker-side subscribe queue depth). const DEFAULT_QUEUE: usize = 256; /// Depth of a route's out-of-band control channel (the `flush` command verb). @@ -76,7 +75,8 @@ struct BuiltRoute { /// Per-route-invariant build context: the shared Rhai engine, the script-file loader, the /// cross-route defaults, and the component identity injected into scripts. Bundled so `build_route` -/// takes the route plus one context, not a long argument list. +/// takes the route plus one context, not a long argument list. Populated from +/// [`crate::route_build::GlobalWiring`] (the actual defaulting logic lives there, tested). struct RouteBuildCtx<'a> { engine: &'a Arc, loader: &'a crate::proc::script::ScriptLoader, @@ -113,39 +113,22 @@ impl ProcessorApp { engine.set_max_operations(1_000_000); let engine = Arc::new(engine); - let defaults: GlobalDefaults = config - .global() - .get("defaults") - .and_then(|v| serde_json::from_value(v.clone()).ok()) - .unwrap_or_default(); - let default_key = defaults.key.clone().unwrap_or_else(|| DEFAULT_KEY.to_string()); - // Script files (`{"file": "..."}`) resolve relative to this dir (template-substituted). - let scripts_dir = defaults - .scripts_dir - .as_deref() - .map(|d| resolve(&config, d)) - .unwrap_or_else(|| ".".to_string()); - let loader = crate::proc::script::ScriptLoader::new(scripts_dir); - // Component identity for the script runtime context. Read the raw values (the template - // resolver would sanitize them for topic/path safety, which we don't want in a script). - let component_full_name = config.component_name.clone(); - let component_name = - component_full_name.rsplit('.').next().unwrap_or(&component_full_name).to_string(); - let thing_name = config.thing_name.clone(); + // Cross-route defaults + the processor's own identity strings — the actual defaulting + // decisions live in `route_build::resolve_global_wiring`, tested there against a plain + // `Config` (no live `EdgeCommons` needed). + let wiring = resolve_global_wiring(&config); + let loader = crate::proc::script::ScriptLoader::new(wiring.scripts_dir.clone()); let ctx = RouteBuildCtx { engine: &engine, loader: &loader, - default_key: &default_key, - default_target: defaults.target.as_deref(), - thing_name: &thing_name, - component_name: &component_name, - component_full_name: &component_full_name, - default_script_engine: defaults.script_engine.unwrap_or_default(), + default_key: &wiring.default_key, + default_target: wiring.default_target.as_deref(), + thing_name: &wiring.thing_name, + component_name: &wiring.component_name, + component_full_name: &wiring.component_full_name, + default_script_engine: wiring.default_script_engine, }; - // The processor's own UNS identity — the self-echo guard's match, and the restamp source. - let own_device = config.identity().device().to_string(); - let own_component = config.identity().component().to_string(); // The `evt` health-event publisher — a thin wrapper over the library's `events()` facade at // component scope (D-U28: no instance token, so events land on `.../telemetry-processor/evt/…`). let evt = EvtEmitter::new(gg.events()); @@ -196,8 +179,8 @@ impl ProcessorApp { &app.messaging, &filter, routes, - own_device.clone(), - own_component.clone(), + wiring.own_device.clone(), + wiring.own_component.clone(), evt.clone(), ) .await?; @@ -235,7 +218,11 @@ impl ProcessorApp { Ok(app) } - /// Build one route's worker + channels; return the wired route (no subscription yet). + /// Build one route's worker + channels; return the wired route (no subscription yet). The + /// route-level decisions (target/filter/publish/script-output-topic resolution, the restamp + /// policy) are delegated to `crate::route_build`, which needs only `config` — the sole thing + /// here that genuinely requires a live `&EdgeCommons` is `gg.streams()` for a `Channel::Stream` + /// target (behind the `streaming` feature). fn build_route( &mut self, gg: &EdgeCommons, @@ -246,61 +233,11 @@ impl ProcessorApp { ) -> anyhow::Result { let _ = gg; // used only under the `streaming` feature (stream targets) let route_key = route.key.clone().unwrap_or_else(|| ctx.default_key.to_string()); - let target_str = route - .target - .clone() - .or_else(|| ctx.default_target.map(String::from)) - .ok_or_else(|| anyhow::anyhow!("route '{}' has no target", route.id))?; - let target = parse_target(&target_str)?; - - anyhow::ensure!(!route.subscribe.is_empty(), "route '{}' has no subscribe topics", route.id); - let filters: Vec = route.subscribe.iter().map(|f| resolve(config, f)).collect(); - - let mut publish = route.publish.clone().unwrap_or_default(); - if let Some(t) = &publish.topic { - let resolved = resolve(config, t); - // Defensive: a publish topic that resolves to a reserved UNS class (state|metric|cfg|log) - // is rejected at publish time by the reserved-class guard (silent drop). Warn at startup. - if let Some(cls) = reserved_class_of(&resolved, config.effective_include_root()) { - tracing::warn!( - route = %route.id, - topic = %resolved, - class = cls.token(), - "publish.topic targets a RESERVED UNS class; the reserved-class guard will drop \ - these publishes — target a data/evt/app class instead" - ); - } - publish.topic = Some(resolved); - } - - // Resolve + validate every script stage's explicit output topic: reserved classes and - // subscribe-overlap feedback loops are startup errors, and a route-level `publish.topic` - // may not silently override a stage output topic. - for sc in route.pipeline.iter_mut() { - let StageConfig::Script(ScriptStageSpec::Spec(sp)) = sc else { continue }; - let Some(out) = sp.output.as_mut() else { continue }; - let resolved = resolve(config, &out.topic); - validate_script_output_topic( - &route.id, - &resolved, - config.effective_include_root(), - &filters, - publish.topic.as_deref(), - )?; - out.topic = resolved; - } - - // Restamp policy: `local` output carries the processor's own identity (instance = route id) - // — loop-safety for the self-echo guard + correct provenance for the processor's product. - let restamp: Option = match &target { - Channel::Local => Some( - config - .identity() - .with_instance(&route.id) - .map_err(|e| anyhow::anyhow!("route '{}' identity restamp: {e}", route.id))?, - ), - _ => None, - }; + let target = resolve_target(&route, ctx.default_target)?; + let filters = resolve_filters(config, &route)?; + let publish = resolve_publish_topic(config, &route.id, route.publish.clone().unwrap_or_default()); + resolve_script_output_topics(config, &mut route, &filters, publish.topic.as_deref())?; + let restamp = compute_restamp(config, &target, &route.id)?; #[cfg(feature = "streaming")] let stream = match &target { diff --git a/src/main.rs b/src/main.rs index 7afe1a4..85d7556 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,7 @@ mod dispatch; mod json_path; mod observe; mod proc; +mod route_build; #[cfg(test)] mod test_support; diff --git a/src/route_build.rs b/src/route_build.rs new file mode 100644 index 0000000..d896897 --- /dev/null +++ b/src/route_build.rs @@ -0,0 +1,349 @@ +//! # Route-build decision logic (testable without a live `EdgeCommons`) +//! +//! Everything here operates on [`Config`] — publicly constructible via `Config::from_value`, so a +//! unit test can build one without a live `EdgeCommons` — plus other plain or already-fakeable +//! inputs ([`Channel`], [`RouteConfig`]). It is split out of +//! [`crate::app::ProcessorApp::start`]/`build_route` specifically so these decision branches stay in +//! the coverage gate's denominator instead of being dropped alongside the genuine composition-root +//! seam (`src/app.rs`'s module doc explains what remains there and why): +//! +//! - [`resolve_global_wiring`] — the cross-route defaulting rules (`global.defaults.key`/ +//! `scriptsDir`/`target`/`scriptEngine`), the component-name split (`ComponentFullName` → +//! `ComponentName`), and the processor's own UNS identity strings (the self-echo guard's match). +//! - [`resolve_target`] — a route's target channel, falling back to the global default. +//! - [`resolve_filters`] — a route's `subscribe` filters, template-resolved, non-empty required. +//! - [`resolve_publish_topic`] — a route's `publish.topic` template resolution, warning (not +//! failing) when it resolves to a reserved UNS class. +//! - [`resolve_script_output_topics`] — every script stage's explicit `output.topic`: resolved and +//! validated (reserved class / subscribe-filter feedback loop / `publish.topic` collision). +//! - [`compute_restamp`] — the `local`-target identity restamp policy. +//! +//! The one piece of route building that genuinely cannot move here is the +//! `#[cfg(feature = "streaming")] gg.streams()` call for a `Channel::Stream` target (`src/app.rs`'s +//! `build_route`) — it needs a live `&EdgeCommons`-obtained `Arc` handle, which no +//! test can fabricate (the library exposes no public/test constructor for one). + +use edgecommons::config::model::Config; +use edgecommons::config::template::resolve; +use edgecommons::facades::Channel; +use edgecommons::messaging::message::MessageIdentity; +use edgecommons::uns::reserved_class_of; + +use crate::config::{parse_target, GlobalDefaults, PublishConfig, RouteConfig, ScriptEngineKind, ScriptStageSpec, StageConfig}; +use crate::dispatch::validate_script_output_topic; + +/// Default aggregation/partition key when neither the route nor the global defaults set one. +pub(crate) const DEFAULT_KEY: &str = "body.signal.id"; + +/// Cross-route defaults + the processor's own identity strings, resolved once from `Config` at +/// startup (no live `EdgeCommons` needed). +pub(crate) struct GlobalWiring { + pub default_key: String, + pub scripts_dir: String, + pub default_target: Option, + pub default_script_engine: ScriptEngineKind, + /// `{ComponentName}` — the short name (segment after the last `.`). + pub component_name: String, + /// `{ComponentFullName}` — the fully-qualified component name. + pub component_full_name: String, + pub thing_name: String, + /// The processor's own UNS device token — the self-echo guard's match. + pub own_device: String, + /// The processor's own UNS component token — the self-echo guard's match. + pub own_component: String, +} + +/// Resolve [`GlobalWiring`] from `component.global.defaults` + the config's own identity/name. +pub(crate) fn resolve_global_wiring(config: &Config) -> GlobalWiring { + let defaults: GlobalDefaults = config + .global() + .get("defaults") + .and_then(|v| serde_json::from_value(v.clone()).ok()) + .unwrap_or_default(); + let default_key = defaults.key.clone().unwrap_or_else(|| DEFAULT_KEY.to_string()); + // Script files (`{"file": "..."}`) resolve relative to this dir (template-substituted). + let scripts_dir = + defaults.scripts_dir.as_deref().map(|d| resolve(config, d)).unwrap_or_else(|| ".".to_string()); + // Component identity for the script runtime context. Read the raw values (the template + // resolver would sanitize them for topic/path safety, which we don't want in a script). + let component_full_name = config.component_name.clone(); + let component_name = + component_full_name.rsplit('.').next().unwrap_or(&component_full_name).to_string(); + let thing_name = config.thing_name.clone(); + // The processor's own UNS identity — the self-echo guard's match, and the restamp source. + let own_device = config.identity().device().to_string(); + let own_component = config.identity().component().to_string(); + GlobalWiring { + default_key, + scripts_dir, + default_target: defaults.target.clone(), + default_script_engine: defaults.script_engine.unwrap_or_default(), + component_name, + component_full_name, + thing_name, + own_device, + own_component, + } +} + +/// Resolve a route's target channel: the route's own `target`, falling back to +/// `global.defaults.target`. `Err` when neither is set, or the string doesn't parse. +pub(crate) fn resolve_target(route: &RouteConfig, default_target: Option<&str>) -> anyhow::Result { + let target_str = route + .target + .clone() + .or_else(|| default_target.map(String::from)) + .ok_or_else(|| anyhow::anyhow!("route '{}' has no target", route.id))?; + parse_target(&target_str) +} + +/// Resolve a route's `subscribe` filters (template-substituted). `Err` when the list is empty. +pub(crate) fn resolve_filters(config: &Config, route: &RouteConfig) -> anyhow::Result> { + anyhow::ensure!(!route.subscribe.is_empty(), "route '{}' has no subscribe topics", route.id); + Ok(route.subscribe.iter().map(|f| resolve(config, f)).collect()) +} + +/// Resolve a route's `publish.topic` template. Defensive-only: a topic that resolves to a reserved +/// UNS class (`state`/`metric`/`cfg`/`log`) is rejected at publish time by the reserved-class guard +/// (silent drop) — this only warns at startup so the drop isn't a silent surprise later. +pub(crate) fn resolve_publish_topic( + config: &Config, + route_id: &str, + mut publish: PublishConfig, +) -> PublishConfig { + if let Some(t) = &publish.topic { + let resolved = resolve(config, t); + if let Some(cls) = reserved_class_of(&resolved, config.effective_include_root()) { + tracing::warn!( + route = %route_id, + topic = %resolved, + class = cls.token(), + "publish.topic targets a RESERVED UNS class; the reserved-class guard will drop \ + these publishes — target a data/evt/app class instead" + ); + } + publish.topic = Some(resolved); + } + publish +} + +/// Resolve + validate every script stage's explicit `output.topic` in `route.pipeline` (mutates it +/// in place): reserved classes and subscribe-overlap feedback loops are startup errors, and a +/// route-level `publish.topic` may not silently override a stage output topic. +pub(crate) fn resolve_script_output_topics( + config: &Config, + route: &mut RouteConfig, + filters: &[String], + publish_topic: Option<&str>, +) -> anyhow::Result<()> { + for sc in route.pipeline.iter_mut() { + let StageConfig::Script(ScriptStageSpec::Spec(sp)) = sc else { continue }; + let Some(out) = sp.output.as_mut() else { continue }; + let resolved = resolve(config, &out.topic); + validate_script_output_topic( + &route.id, + &resolved, + config.effective_include_root(), + filters, + publish_topic, + )?; + out.topic = resolved; + } + Ok(()) +} + +/// The `local`-target identity restamp policy: `Some(own identity, instance = route id)` for +/// `Channel::Local` (loop-safety for the self-echo guard + correct provenance for the processor's +/// product); `None` for every other target (they preserve the source identity for provenance). +pub(crate) fn compute_restamp( + config: &Config, + target: &Channel, + route_id: &str, +) -> anyhow::Result> { + match target { + Channel::Local => Ok(Some( + config + .identity() + .with_instance(route_id) + .map_err(|e| anyhow::anyhow!("route '{route_id}': identity restamp: {e}"))?, + )), + _ => Ok(None), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn config(raw: serde_json::Value) -> Config { + Config::from_value("com.mbreissi.edgecommons.TelemetryProcessor", "gw-01", raw).unwrap() + } + + fn route(v: serde_json::Value) -> RouteConfig { + serde_json::from_value(v).unwrap() + } + + // ---- resolve_global_wiring ---------------------------------------------------------------- + + #[test] + fn global_wiring_defaults_when_component_global_is_absent() { + let cfg = config(json!({})); + let w = resolve_global_wiring(&cfg); + assert_eq!(w.default_key, DEFAULT_KEY); + assert_eq!(w.scripts_dir, "."); + assert_eq!(w.default_target, None); + assert_eq!(w.default_script_engine, ScriptEngineKind::Rhai); + assert_eq!(w.component_full_name, "com.mbreissi.edgecommons.TelemetryProcessor"); + assert_eq!(w.component_name, "TelemetryProcessor", "the short name after the last '.'"); + assert_eq!(w.thing_name, "gw-01"); + assert_eq!(w.own_device, "gw-01", "zero-config identity defaults device to the thing name"); + } + + #[test] + fn global_wiring_reads_component_global_defaults() { + let cfg = config(json!({ + "component": { "global": { "defaults": { + "key": "body.custom.id", + "target": "northbound", + "scriptEngine": "lua" + } } } + })); + let w = resolve_global_wiring(&cfg); + assert_eq!(w.default_key, "body.custom.id"); + assert_eq!(w.default_target.as_deref(), Some("northbound")); + assert_eq!(w.default_script_engine, ScriptEngineKind::Lua); + } + + #[test] + fn global_wiring_resolves_a_templated_scripts_dir() { + let cfg = config(json!({ + "tags": { "site": "dallas" }, + "component": { "global": { "defaults": { "scriptsDir": "scripts/{site}" } } } + })); + let w = resolve_global_wiring(&cfg); + assert_eq!(w.scripts_dir, "scripts/dallas"); + } + + // ---- resolve_target ------------------------------------------------------------------------ + + #[test] + fn resolve_target_uses_the_route_target_or_falls_back_to_the_default() { + let r = route(json!({ "id": "r1", "subscribe": ["a"], "target": "local" })); + assert_eq!(resolve_target(&r, None).unwrap(), Channel::Local); + + let r = route(json!({ "id": "r1", "subscribe": ["a"] })); + assert_eq!(resolve_target(&r, Some("northbound")).unwrap(), Channel::Northbound); + + let r = route(json!({ "id": "r1", "subscribe": ["a"] })); + let err = resolve_target(&r, None).unwrap_err(); + assert!(err.to_string().contains("no target")); + } + + // ---- resolve_filters ----------------------------------------------------------------------- + + #[test] + fn resolve_filters_resolves_templates_and_rejects_an_empty_list() { + let cfg = config(json!({})); + let r = route(json!({ "id": "r1", "subscribe": ["ecv1/+/+/+/data/#"] })); + assert_eq!(resolve_filters(&cfg, &r).unwrap(), vec!["ecv1/+/+/+/data/#".to_string()]); + + let r = route(json!({ "id": "r1", "subscribe": [] })); + let err = resolve_filters(&cfg, &r).unwrap_err(); + assert!(err.to_string().contains("no subscribe topics")); + } + + // ---- resolve_publish_topic ------------------------------------------------------------------- + + #[test] + fn resolve_publish_topic_resolves_templates_and_passes_through_a_reserved_class() { + let cfg = config(json!({ "tags": { "site": "dallas" } })); + let pub_cfg = PublishConfig { + topic: Some("ecv1/{ThingName}/telemetry-processor/data/{site}".to_string()), + partition_key: None, + qos: None, + }; + let out = resolve_publish_topic(&cfg, "r1", pub_cfg); + assert_eq!(out.topic.as_deref(), Some("ecv1/gw-01/telemetry-processor/data/dallas")); + + // A reserved-class topic still resolves (only a startup WARN, not a failure — the + // reserved-class guard is what actually drops the publish at runtime). + let reserved = PublishConfig { + topic: Some("ecv1/{ThingName}/telemetry-processor/metric/derived".to_string()), + partition_key: None, + qos: None, + }; + let out = resolve_publish_topic(&cfg, "r1", reserved); + assert_eq!(out.topic.as_deref(), Some("ecv1/gw-01/telemetry-processor/metric/derived")); + } + + #[test] + fn resolve_publish_topic_is_a_passthrough_with_no_topic() { + let cfg = config(json!({})); + let out = resolve_publish_topic(&cfg, "r1", PublishConfig::default()); + assert_eq!(out.topic, None); + } + + // ---- resolve_script_output_topics ----------------------------------------------------------- + + #[test] + fn resolve_script_output_topics_resolves_and_validates_in_place() { + let cfg = config(json!({})); + let mut r = route(json!({ + "id": "r1", + "subscribe": ["a"], + "pipeline": [ { "script": { + "source": "body", + "output": { "topic": "ecv1/{ThingName}/telemetry-processor/r1/data/derived" } + } } ] + })); + resolve_script_output_topics(&cfg, &mut r, &[], None).unwrap(); + let StageConfig::Script(ScriptStageSpec::Spec(sp)) = &r.pipeline[0] else { panic!() }; + assert_eq!( + sp.output.as_ref().unwrap().topic, + "ecv1/gw-01/telemetry-processor/r1/data/derived", + "the template must be resolved in place" + ); + } + + #[test] + fn resolve_script_output_topics_rejects_a_feedback_loop() { + let cfg = config(json!({})); + let mut r = route(json!({ + "id": "r1", + "subscribe": ["ecv1/+/+/+/data/#"], + "pipeline": [ { "script": { + "source": "body", + "output": { "topic": "ecv1/gw-01/telemetry-processor/r1/data/derived" } + } } ] + })); + let filters = vec!["ecv1/+/+/+/data/#".to_string()]; + let err = resolve_script_output_topics(&cfg, &mut r, &filters, None).unwrap_err(); + assert!(err.to_string().contains("feedback loop"), "{err}"); + } + + #[test] + fn resolve_script_output_topics_is_a_noop_without_a_multi_signal_output() { + let cfg = config(json!({})); + let mut r = route(json!({ + "id": "r1", + "subscribe": ["a"], + "pipeline": [ { "script": "body" } ] + })); + resolve_script_output_topics(&cfg, &mut r, &[], None).unwrap(); + } + + // ---- compute_restamp ----------------------------------------------------------------------- + + #[test] + fn compute_restamp_restamps_only_the_local_target() { + let cfg = config(json!({})); + let restamp = compute_restamp(&cfg, &Channel::Local, "r1").unwrap(); + let identity = restamp.expect("local target restamps"); + assert_eq!(identity.instance(), Some("r1")); + assert_eq!(identity.device(), "gw-01"); + + assert!(compute_restamp(&cfg, &Channel::Northbound, "r1").unwrap().is_none()); + assert!(compute_restamp(&cfg, &Channel::Stream("archive".into()), "r1").unwrap().is_none()); + } +}