mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-22 12:23:04 -07:00
Compare commits
469 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd88996f7f | |||
| 1643cf86ed | |||
| 6e8be1af22 | |||
| 9d18e2db9b | |||
| d2b8992932 | |||
| f4910efd7d | |||
| 1b39d21bbe | |||
| cc9ebfaf42 | |||
| 9347b42c9a | |||
| cb74087f27 | |||
| 086bfd9938 | |||
| da7671744f | |||
| abcb238022 | |||
| dc32bd480f | |||
| 4663d13194 | |||
| 9cb5f2c880 | |||
| 2b8a0a8cf7 | |||
| 6f879a5551 | |||
| 1068ad4e8f | |||
| 9b42b40a36 | |||
| 43f3be4924 | |||
| a7e41df1c6 | |||
| f1749472e7 | |||
| 66494c081c | |||
| 6c8afc942c | |||
| 1dcbbd801b | |||
| 76869ed2b6 | |||
| b24bfdc15c | |||
| e543e4a5db | |||
| 9b639ef7d1 | |||
| 07bc2d42b8 | |||
| 7a0b4b5890 | |||
| 2210443e37 | |||
| 8bf6570843 | |||
| 26a3b0f5e8 | |||
| 741c957f31 | |||
| e4496742a4 | |||
| ce00de5da8 | |||
| f5c50e69fc | |||
| 9709c2040d | |||
| 3faa989691 | |||
| 84e924b77e | |||
| c5b16e7048 | |||
| c1335cec31 | |||
| bdc3ba1df6 | |||
| 6afce0bbdc | |||
| 327873d010 | |||
| 08175009d2 | |||
| a5d3be465e | |||
| fd2b93367d | |||
| 2a93f51e81 | |||
| 008143ff00 | |||
| d340855c8b | |||
| 78d6d9d6f1 | |||
| 5cc85b0619 | |||
| 7433ce0d0e | |||
| 75a97b4da9 | |||
| c23e0f2a3c | |||
| 08ba4ad996 | |||
| 39da441d14 | |||
| 904ec93668 | |||
| 4cd8d9eb56 | |||
| 283baca848 | |||
| 765261648d | |||
| c3cef71aa3 | |||
| 18d9c166d8 | |||
| 286256ebf0 | |||
| 12aae503c9 | |||
| 95e5168244 | |||
| 5fd9fff9cf | |||
| db5b3887f9 | |||
| 5a3e1b4e6e | |||
| 21a0226a19 | |||
| c5c49f62d1 | |||
| dac66c988d | |||
| 303d168681 | |||
| 1ddb3385e2 | |||
| eb75274dbf | |||
| 3a7887348c | |||
| 0a4cb0601f | |||
| 861e29277c | |||
| c76b149ef9 | |||
| 4c4c6fc840 | |||
| 0c14dfe924 | |||
| 17e531b4ee | |||
| f022f62cce | |||
| e91f1386b1 | |||
| 02f543af38 | |||
| 20c96fb551 | |||
| acd3d6f425 | |||
| 2b15a24b6d | |||
| 7fac0bc613 | |||
| 62f51761ee | |||
| 5340cc288e | |||
| befe3c8fb7 | |||
| 41ec24c81e | |||
| 42b497ff65 | |||
| 01d908a560 | |||
| 42debcce80 | |||
| 8bc993eceb | |||
| 366ac33e23 | |||
| b5a7023bd3 | |||
| 883b38c77c | |||
| 59c767a9e2 | |||
| 9b5bb848f7 | |||
| 5bf06530ce | |||
| 768e6870cb | |||
| 79829ddd53 | |||
| 78082801c6 | |||
| 50771ddccc | |||
| 3a8a9ddecc | |||
| 6cd45c1f1f | |||
| 1a2db43cf5 | |||
| 4840e564f4 | |||
| 744dce932c | |||
| 8dfc1bc932 | |||
| d92cf43c57 | |||
| 099699872e | |||
| 5099903043 | |||
| 982fe47a33 | |||
| 65d5fadd13 | |||
| b55f5255ad | |||
| 83edef4806 | |||
| d4936d889a | |||
| c938cc8eae | |||
| 0558834eef | |||
| 098950fdde | |||
| 91e68a1d1e | |||
| 7172ddb247 | |||
| 96f2e058f7 | |||
| 8782944191 | |||
| ae26db6df2 | |||
| d038141a8a | |||
| f6960c61d6 | |||
| 07fa2d2c9a | |||
| 82c6d69a0b | |||
| d4dc1b9e49 | |||
| 24d2b7b142 | |||
| b6e56c4e9f | |||
| 45c77a4c3b | |||
| 09af190ac0 | |||
| d24f3691cb | |||
| daaaa15483 | |||
| 041652d85d | |||
| 17570e12b8 | |||
| 78172734db | |||
| 19d4a193ff | |||
| 66680368b6 | |||
| b4ded21ea3 | |||
| 7412373d8a | |||
| 259960b80b | |||
| 18bb4186a8 | |||
| 6d3307c0df | |||
| 6eea20b89a | |||
| 5077cefda8 | |||
| 14d7adfdd5 | |||
| 000027fab8 | |||
| ade23795b8 | |||
| 67ad33b07a | |||
| d54874d3a4 | |||
| ec6420254a | |||
| 74fff13d18 | |||
| 0d2deb1b63 | |||
| c4c0004c4a | |||
| a59cdfef7c | |||
| f495451b34 | |||
| c53c6560fa | |||
| d6def7643d | |||
| fef7a24951 | |||
| 514b0513de | |||
| 514fdc40ee | |||
| f731f0d9d0 | |||
| fbff230c86 | |||
| fdaa5032a9 | |||
| ef491a3a66 | |||
| 926721c482 | |||
| 8859de5393 | |||
| 2991562234 | |||
| b45c6ec05f | |||
| 4b3aaee03b | |||
| 1ed4f258b4 | |||
| 485f118a5f | |||
| 573336ed80 | |||
| 143aa90b18 | |||
| b807b50a64 | |||
| 147a3c7593 | |||
| a7bbfda799 | |||
| f683adba13 | |||
| 17106f887a | |||
| 8f93ff9f68 | |||
| 1d671ea41f | |||
| b8e57f4788 | |||
| 19bd17566f | |||
| 2ce6a7cee2 | |||
| 45de61b438 | |||
| 8910c0988e | |||
| 1e68c160a1 | |||
| 2df9ee4a1d | |||
| b18cca92ab | |||
| d8b55340f7 | |||
| 92e1a0ccaf | |||
| 24f344c0b1 | |||
| 455dc683eb | |||
| b397b811f9 | |||
| 04ddc6223e | |||
| 42540fba99 | |||
| f62943199c | |||
| 5609e6c010 | |||
| 5848d25612 | |||
| ae067739ce | |||
| ae2dd43073 | |||
| bc06567bb0 | |||
| bdb0c0878e | |||
| b74319bf10 | |||
| d3721b0020 | |||
| ad51280e51 | |||
| f1c0435bce | |||
| 43229bf79f | |||
| c5a270aabc | |||
| 46d85d397d | |||
| c1565c5f42 | |||
| fdf8661a4b | |||
| 6e5b2c0e63 | |||
| 9626c7de32 | |||
| 9e36a4188a | |||
| 0d177494d9 | |||
| 9d365f4bbb | |||
| f705cc04a9 | |||
| 7bcc32fea1 | |||
| d53e533c9f | |||
| b4278842d9 | |||
| a0d378d06d | |||
| 0795c1bbf8 | |||
| 3709ceff8e | |||
| b2a1251774 | |||
| 2b31c7f6b7 | |||
| c83955eea7 | |||
| c2135a7066 | |||
| 90078760c1 | |||
| b97f32f86e | |||
| 71dd7e9852 | |||
| 984122f394 | |||
| c5d63b3090 | |||
| 6a728a3357 | |||
| 3e29328949 | |||
| f9c86bc308 | |||
| d50c6e0a73 | |||
| db1dce0f3b | |||
| ed0c9ade1a | |||
| 9aed86cbf2 | |||
| a3238304f5 | |||
| b88f4762a5 | |||
| 8f93a5947e | |||
| 5ede3dc416 | |||
| 64ef63a056 | |||
| 46ac55d950 | |||
| 961dea6934 | |||
| cc51cc81f9 | |||
| 362e8d1603 | |||
| cba3b7dc38 | |||
| e4bd11317a | |||
| 0da380a55b | |||
| 3e8cf4a975 | |||
| 0bff57fb43 | |||
| c2240c7a60 | |||
| bb2458c765 | |||
| d55377e169 | |||
| a4857ee8f4 | |||
| 7f1f6044dc | |||
| 6bb5c63db7 | |||
| cf6c755e51 | |||
| 81ab1886d1 | |||
| 90f2d64019 | |||
| a0efe491e5 | |||
| ee59731ed2 | |||
| 2df549f1f8 | |||
| efefd39439 | |||
| 9bea14b341 | |||
| cbad78962f | |||
| d4faedfba1 | |||
| bcebf1cdc5 | |||
| 1011825949 | |||
| bf07570848 | |||
| 5a73f1a88e | |||
| 7b60a5b060 | |||
| a29ae29487 | |||
| 011e49e1cc | |||
| 9507eb3de5 | |||
| 9a2ee0273f | |||
| 8c32ad2483 | |||
| 7c80bb0612 | |||
| fe2b11c88e | |||
| 92cb184a5c | |||
| a935573ef8 | |||
| 266342cd98 | |||
| 2ae542ecdb | |||
| eedb8d22c1 | |||
| 6f2a87be4f | |||
| ef0b77baa8 | |||
| 9e23de4ba1 | |||
| 891f0dad9e | |||
| 730e8bb4d4 | |||
| 91b7f86225 | |||
| 0d63724903 | |||
| 269c1d5fdf | |||
| 28f6b0f18b | |||
| 35df8d99dc | |||
| 0628f08e6b | |||
| ccb2db2309 | |||
| 4e7cd9ab6f | |||
| 4d97cec869 | |||
| 7cb1bfa667 | |||
| 159c983a3f | |||
| 4abb00b86d | |||
| 7bf0220f25 | |||
| e10013fd2c | |||
| a6664bbb93 | |||
| 1750c06369 | |||
| a2bd7ca299 | |||
| 85c7933ad6 | |||
| d5ec291579 | |||
| 6845ad409b | |||
| e7a5ab9450 | |||
| c75421f46e | |||
| 72c17096ea | |||
| 78fc5ffcf7 | |||
| cccaf6b206 | |||
| 9e4fe62de2 | |||
| f74115c6e2 | |||
| cefc8cfd42 | |||
| 3b7aa8242a | |||
| be0d749f9c | |||
| 2128aab6ca | |||
| f559e4027e | |||
| 4352868410 | |||
| f04b548f8c | |||
| 2f9dd47cc2 | |||
| 87f0c2c084 | |||
| fb7c92da79 | |||
| 2377f51718 | |||
| ff2c29c34f | |||
| 4a06caec67 | |||
| 2a79211aee | |||
| cd5334215a | |||
| dfcb04484b | |||
| d18c872072 | |||
| 80b2c636b0 | |||
| b779edc0d6 | |||
| 3bc20a0a46 | |||
| 121928bc57 | |||
| 1d63b8901d | |||
| 474c430ad1 | |||
| f968ae4fd4 | |||
| aa61e327f6 | |||
| 605a8b86b8 | |||
| ba60b7e4f6 | |||
| 9cba9bfec4 | |||
| ed10e21ee9 | |||
| 9d8fcbe866 | |||
| afe4123a17 | |||
| bbba8f4373 | |||
| 897aab032e | |||
| 5b2c83ae6e | |||
| dc15cceb1e | |||
| b5c2d6ce9e | |||
| 0eeda63abb | |||
| d4933ae314 | |||
| 53ffe0e06c | |||
| 0433e3b256 | |||
| 9b409799c8 | |||
| dd96709d18 | |||
| 3818a72045 | |||
| 0437ce1bb4 | |||
| 0d5d7da70f | |||
| 277a0eb6a7 | |||
| c02fc37491 | |||
| 1d440be352 | |||
| 67b2897a8c | |||
| 519e7c4179 | |||
| 36bc1fb491 | |||
| 9e3fe4e557 | |||
| a6d8278730 | |||
| b23d20ea05 | |||
| cf4bc470e4 | |||
| da923e409a | |||
| f7d7c5704a | |||
| f03bbd9a92 | |||
| ff5bb770d7 | |||
| 8dd350264a | |||
| cde090685a | |||
| a9f1dad091 | |||
| 54827cd0a2 | |||
| e01bb53b2e | |||
| 9f2b808cdb | |||
| 6709ded66c | |||
| fecaf0f400 | |||
| 730e83472a | |||
| 88145d08e5 | |||
| c367802b4a | |||
| 3d36524707 | |||
| 6cdc5879bb | |||
| 79d14cd260 | |||
| f6020b32a7 | |||
| aa5c4a8d69 | |||
| ec1f2de5cf | |||
| 3d01822d27 | |||
| f066fcda32 | |||
| b3b4df0fc7 | |||
| 616a97d242 | |||
| d9dabb4a96 | |||
| 371fb2cb17 | |||
| 5c824e50b8 | |||
| fbe99e33cd | |||
| 35bf1afcff | |||
| 543cde525e | |||
| dad7780ab8 | |||
| eb941778f2 | |||
| b7acce6527 | |||
| 247d3c758b | |||
| 79f7e89740 | |||
| 8d7bcbd947 | |||
| 23a59806c2 | |||
| 1e76e137ab | |||
| cef03c495f | |||
| 36b56a400c | |||
| c6f63fd4a2 | |||
| 7cdf47a9e4 | |||
| 9b706dfaee | |||
| f7bfe5ecaa | |||
| 6ef43ce7ff | |||
| 3c87d36535 | |||
| a62a377081 | |||
| b557477770 | |||
| bf13249003 | |||
| 31c5a5dde5 | |||
| 758256a1a2 | |||
| c660cb4e89 | |||
| 0512dcaf4f | |||
| d1075afc02 | |||
| f037f01b27 | |||
| 65e563a889 | |||
| bd18297af3 | |||
| 77505ca7cb | |||
| c22c16044c | |||
| 889a70efdd | |||
| 2386020639 | |||
| 60adac0eb7 | |||
| 95686ae858 | |||
| fd4cf5d414 | |||
| 49794c5e04 | |||
| e29387f3c1 | |||
| 581a800612 | |||
| 1456f47fd1 | |||
| a9b2da86ff | |||
| 6c67dc4a98 | |||
| 2edd9ed2d7 | |||
| 9dda513f84 | |||
| 5ecfd6cd42 | |||
| f494486e12 | |||
| 9613fce919 | |||
| 486871379c | |||
| fba0550dda | |||
| 371ff86287 | |||
| c90953adbe | |||
| 4031bf3e79 | |||
| 9adaff488a | |||
| 9f6168915f | |||
| 64b90dd678 | |||
| 93e02aed44 |
@@ -1,2 +1,5 @@
|
||||
[build]
|
||||
rustflags = ["-C", "target-cpu=native"]
|
||||
|
||||
[alias]
|
||||
dev = "run -p brk_cli --features brk_server/bindgen"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.git
|
||||
target
|
||||
docker
|
||||
@@ -0,0 +1,15 @@
|
||||
name: Check outdated dependencies
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 9 * * 1"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
outdated:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: cargo install cargo-outdated
|
||||
- run: cargo outdated --exit-code 1 --depth 1
|
||||
@@ -20,6 +20,11 @@ _*
|
||||
/*.html
|
||||
/research
|
||||
/filter_*
|
||||
/heatmaps*
|
||||
/oracle*
|
||||
/playground
|
||||
/*.txt
|
||||
/*.csv
|
||||
|
||||
# Logs
|
||||
*.log*
|
||||
@@ -34,6 +39,7 @@ flamegraph.svg
|
||||
|
||||
# AI
|
||||
.claude/settings*
|
||||
!CLAUDE.md
|
||||
|
||||
# Expand
|
||||
expand.rs
|
||||
|
||||
Generated
+720
-694
File diff suppressed because it is too large
Load Diff
+44
-35
@@ -4,7 +4,7 @@ members = ["crates/*"]
|
||||
package.description = "The Bitcoin Research Kit is a suite of tools designed to extract, compute and display data stored on a Bitcoin Core node"
|
||||
package.license = "MIT"
|
||||
package.edition = "2024"
|
||||
package.version = "0.1.0-alpha.5"
|
||||
package.version = "0.3.0-beta.8"
|
||||
package.homepage = "https://bitcoinresearchkit.org"
|
||||
package.repository = "https://github.com/bitcoinresearchkit/brk"
|
||||
package.readme = "README.md"
|
||||
@@ -36,49 +36,57 @@ inherits = "release"
|
||||
debug = true
|
||||
|
||||
[workspace.dependencies]
|
||||
aide = { version = "0.16.0-alpha.2", features = ["axum-json", "axum-query"] }
|
||||
axum = { version = "0.8.8", default-features = false, features = ["http1", "json", "query", "tokio", "tracing"] }
|
||||
bitcoin = { version = "0.32.8", features = ["serde"] }
|
||||
bitcoincore-rpc = "0.19.0"
|
||||
brk_alloc = { version = "0.1.0-alpha.5", path = "crates/brk_alloc" }
|
||||
brk_bencher = { version = "0.1.0-alpha.5", path = "crates/brk_bencher" }
|
||||
brk_bindgen = { version = "0.1.0-alpha.5", path = "crates/brk_bindgen" }
|
||||
brk_cli = { version = "0.1.0-alpha.5", path = "crates/brk_cli" }
|
||||
brk_client = { version = "0.1.0-alpha.5", path = "crates/brk_client" }
|
||||
brk_cohort = { version = "0.1.0-alpha.5", path = "crates/brk_cohort" }
|
||||
brk_computer = { version = "0.1.0-alpha.5", path = "crates/brk_computer" }
|
||||
brk_error = { version = "0.1.0-alpha.5", path = "crates/brk_error" }
|
||||
brk_fetcher = { version = "0.1.0-alpha.5", path = "crates/brk_fetcher" }
|
||||
brk_indexer = { version = "0.1.0-alpha.5", path = "crates/brk_indexer" }
|
||||
brk_iterator = { version = "0.1.0-alpha.5", path = "crates/brk_iterator" }
|
||||
brk_logger = { version = "0.1.0-alpha.5", path = "crates/brk_logger" }
|
||||
brk_mempool = { version = "0.1.0-alpha.5", path = "crates/brk_mempool" }
|
||||
brk_query = { version = "0.1.0-alpha.5", path = "crates/brk_query", features = ["tokio"] }
|
||||
brk_reader = { version = "0.1.0-alpha.5", path = "crates/brk_reader" }
|
||||
brk_rpc = { version = "0.1.0-alpha.5", path = "crates/brk_rpc" }
|
||||
brk_server = { version = "0.1.0-alpha.5", path = "crates/brk_server" }
|
||||
brk_store = { version = "0.1.0-alpha.5", path = "crates/brk_store" }
|
||||
brk_traversable = { version = "0.1.0-alpha.5", path = "crates/brk_traversable", features = ["pco", "derive"] }
|
||||
brk_traversable_derive = { version = "0.1.0-alpha.5", path = "crates/brk_traversable_derive" }
|
||||
brk_types = { version = "0.1.0-alpha.5", path = "crates/brk_types" }
|
||||
byteview = "0.10.0"
|
||||
aide = { version = "0.16.0-alpha.4", features = ["axum-json", "axum-query"] }
|
||||
axum = { version = "0.8.9", default-features = false, features = ["http1", "json", "query", "tokio", "tracing"] }
|
||||
bitcoin = { version = "0.32.9", features = ["serde"] }
|
||||
brk_alloc = { version = "0.3.0-beta.8", path = "crates/brk_alloc" }
|
||||
brk_bencher = { version = "0.3.0-beta.8", path = "crates/brk_bencher" }
|
||||
brk_bindgen = { version = "0.3.0-beta.8", path = "crates/brk_bindgen" }
|
||||
brk_cli = { version = "0.3.0-beta.8", path = "crates/brk_cli" }
|
||||
brk_client = { version = "0.3.0-beta.8", path = "crates/brk_client" }
|
||||
brk_cohort = { version = "0.3.0-beta.8", path = "crates/brk_cohort" }
|
||||
brk_computer = { version = "0.3.0-beta.8", path = "crates/brk_computer" }
|
||||
brk_error = { version = "0.3.0-beta.8", path = "crates/brk_error" }
|
||||
brk_fetcher = { version = "0.3.0-beta.8", path = "crates/brk_fetcher" }
|
||||
brk_indexer = { version = "0.3.0-beta.8", path = "crates/brk_indexer" }
|
||||
brk_iterator = { version = "0.3.0-beta.8", path = "crates/brk_iterator" }
|
||||
brk_logger = { version = "0.3.0-beta.8", path = "crates/brk_logger" }
|
||||
brk_mempool = { version = "0.3.0-beta.8", path = "crates/brk_mempool" }
|
||||
brk_oracle = { version = "0.3.0-beta.8", path = "crates/brk_oracle" }
|
||||
brk_query = { version = "0.3.0-beta.8", path = "crates/brk_query", features = ["tokio"] }
|
||||
brk_reader = { version = "0.3.0-beta.8", path = "crates/brk_reader" }
|
||||
brk_rpc = { version = "0.3.0-beta.8", path = "crates/brk_rpc" }
|
||||
brk_server = { version = "0.3.0-beta.8", path = "crates/brk_server" }
|
||||
brk_store = { version = "0.3.0-beta.8", path = "crates/brk_store" }
|
||||
brk_traversable = { version = "0.3.0-beta.8", path = "crates/brk_traversable", features = ["pco", "derive"] }
|
||||
brk_traversable_derive = { version = "0.3.0-beta.8", path = "crates/brk_traversable_derive" }
|
||||
brk_types = { version = "0.3.0-beta.8", path = "crates/brk_types" }
|
||||
brk_website = { version = "0.3.0-beta.8", path = "crates/brk_website" }
|
||||
byteview = "0.10.1"
|
||||
color-eyre = "0.6.5"
|
||||
corepc-jsonrpc = { package = "jsonrpc", version = "0.19.0", features = ["simple_http"], default-features = false }
|
||||
corepc-types = { version = "0.12.0", features = ["std"], default-features = false }
|
||||
derive_more = { version = "2.1.1", features = ["deref", "deref_mut"] }
|
||||
fjall = "3.0.1"
|
||||
jiff = { version = "0.2.18", features = ["perf-inline", "tz-system"], default-features = false }
|
||||
minreq = { version = "2.14.1", features = ["https", "serde_json"] }
|
||||
fjall = "=3.0.4"
|
||||
indexmap = { version = "2.14.0", features = ["serde"] }
|
||||
jiff = { version = "0.2.24", features = ["perf-inline", "tz-system"], default-features = false }
|
||||
owo-colors = "4.3.0"
|
||||
parking_lot = "0.12.5"
|
||||
rayon = "1.11.0"
|
||||
rustc-hash = "2.1.1"
|
||||
schemars = "1.2.0"
|
||||
pco = "1.0.1"
|
||||
rayon = "1.12.0"
|
||||
rustc-hash = "2.1.2"
|
||||
schemars = { version = "1.2.1", features = ["indexmap2"] }
|
||||
serde = "1.0.228"
|
||||
serde_bytes = "0.11.19"
|
||||
serde_derive = "1.0.228"
|
||||
serde_json = { version = "1.0.149", features = ["float_roundtrip", "preserve_order"] }
|
||||
smallvec = "1.15.1"
|
||||
tokio = { version = "1.49.0", features = ["rt-multi-thread"] }
|
||||
tokio = { version = "1.52.2", features = ["rt-multi-thread"] }
|
||||
tower-http = { version = "0.6.10", features = ["catch-panic", "compression-br", "compression-gzip", "compression-zstd", "cors", "normalize-path", "timeout", "trace"] }
|
||||
tower-layer = "0.3"
|
||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||
vecdb = { version = "0.5.11", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||
ureq = { version = "3.3.0", features = ["json"] }
|
||||
vecdb = { version = "0.10.3", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||
# vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||
|
||||
[workspace.metadata.release]
|
||||
@@ -86,6 +94,7 @@ shared-version = true
|
||||
tag-name = "v{{version}}"
|
||||
pre-release-commit-message = "release: v{{version}}"
|
||||
tag-message = "release: v{{version}}"
|
||||
allow-branch = ["main", "next"]
|
||||
|
||||
[workspace.metadata.dist]
|
||||
cargo-dist-version = "0.30.2"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
*.md
|
||||
!README.md
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "blk"
|
||||
description = "A CLI to inspect Bitcoin Core blocks"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
bitcoin = { workspace = true }
|
||||
brk_error = { workspace = true }
|
||||
brk_reader = { workspace = true }
|
||||
brk_rpc = { workspace = true }
|
||||
brk_types = { workspace = true }
|
||||
owo-colors = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[[bin]]
|
||||
name = "blk"
|
||||
path = "src/main.rs"
|
||||
@@ -0,0 +1,27 @@
|
||||
# blk
|
||||
|
||||
A CLI to inspect Bitcoin Core blocks.
|
||||
|
||||
Reads `blk*.dat` files directly via [`brk_reader`](../brk_reader) and resolves
|
||||
the chain tip / heights via the Bitcoin Core RPC. Output is shell-friendly:
|
||||
bare values, NDJSON, pretty JSON, or TSV.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
cargo install --path crates/blk
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```sh
|
||||
blk 800000 hash # bare hash
|
||||
blk 800000 height hash time # one compact JSON line
|
||||
blk 800000 tx.0.vout.0.value # coinbase output 0 sats
|
||||
blk 0..2 hash tx.0.txid # 3 NDJSON lines
|
||||
blk tip tx.0 # whole coinbase tx as JSON
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
Run `blk --help` for the full field/selector/option reference.
|
||||
@@ -0,0 +1,123 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use brk_error::{Error, Result};
|
||||
use brk_rpc::{Auth, Client};
|
||||
|
||||
use crate::path::Path;
|
||||
|
||||
pub struct Args {
|
||||
pub selector: String,
|
||||
pub paths: Vec<Path>,
|
||||
pub pretty: bool,
|
||||
pub compact: bool,
|
||||
bitcoindir: Option<PathBuf>,
|
||||
blocksdir: Option<PathBuf>,
|
||||
rpcconnect: Option<String>,
|
||||
rpcport: Option<u16>,
|
||||
rpccookiefile: Option<PathBuf>,
|
||||
rpcuser: Option<String>,
|
||||
rpcpassword: Option<String>,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
pub fn parse(raw: Vec<String>) -> Result<Self> {
|
||||
let mut pretty = false;
|
||||
let mut compact = false;
|
||||
let mut bitcoindir = None;
|
||||
let mut blocksdir = None;
|
||||
let mut rpcconnect = None;
|
||||
let mut rpcport = None;
|
||||
let mut rpccookiefile = None;
|
||||
let mut rpcuser = None;
|
||||
let mut rpcpassword = None;
|
||||
let mut positional: Vec<String> = Vec::new();
|
||||
let mut iter = raw.into_iter();
|
||||
while let Some(a) = iter.next() {
|
||||
if a == "-p" || a == "--pretty" {
|
||||
pretty = true;
|
||||
continue;
|
||||
}
|
||||
if a == "-c" || a == "--compact" {
|
||||
compact = true;
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = a.strip_prefix("--") {
|
||||
let (key, value) = match rest.split_once('=') {
|
||||
Some((k, v)) => (k.to_string(), v.to_string()),
|
||||
None => (
|
||||
rest.to_string(),
|
||||
iter.next()
|
||||
.ok_or_else(|| Error::Parse(format!("--{rest} requires a value")))?,
|
||||
),
|
||||
};
|
||||
match key.as_str() {
|
||||
"bitcoindir" => bitcoindir = Some(PathBuf::from(value)),
|
||||
"blocksdir" => blocksdir = Some(PathBuf::from(value)),
|
||||
"rpcconnect" => rpcconnect = Some(value),
|
||||
"rpcport" => {
|
||||
rpcport = Some(value.parse().map_err(|_| {
|
||||
Error::Parse(format!("--rpcport: '{value}' is not a valid port"))
|
||||
})?);
|
||||
}
|
||||
"rpccookiefile" => rpccookiefile = Some(PathBuf::from(value)),
|
||||
"rpcuser" => rpcuser = Some(value),
|
||||
"rpcpassword" => rpcpassword = Some(value),
|
||||
other => return Err(Error::Parse(format!("unknown flag --{other}"))),
|
||||
}
|
||||
continue;
|
||||
}
|
||||
positional.push(a);
|
||||
}
|
||||
|
||||
let mut iter = positional.into_iter();
|
||||
let selector = iter
|
||||
.next()
|
||||
.ok_or_else(|| Error::Parse("missing selector".into()))?;
|
||||
let paths: Vec<Path> = iter.map(|f| Path::parse(&f)).collect::<Result<_>>()?;
|
||||
Ok(Self {
|
||||
selector,
|
||||
paths,
|
||||
pretty,
|
||||
compact,
|
||||
bitcoindir,
|
||||
blocksdir,
|
||||
rpcconnect,
|
||||
rpcport,
|
||||
rpccookiefile,
|
||||
rpcuser,
|
||||
rpcpassword,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn bitcoin_dir(&self) -> PathBuf {
|
||||
self.bitcoindir
|
||||
.clone()
|
||||
.unwrap_or_else(Client::default_bitcoin_path)
|
||||
}
|
||||
|
||||
pub fn blocks_dir(&self) -> PathBuf {
|
||||
self.blocksdir
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.bitcoin_dir().join("blocks"))
|
||||
}
|
||||
|
||||
pub fn rpc(&self) -> Result<Client> {
|
||||
let host = self.rpcconnect.as_deref().unwrap_or("localhost");
|
||||
let port = self.rpcport.unwrap_or(8332);
|
||||
let url = format!("http://{host}:{port}");
|
||||
let cookie = self
|
||||
.rpccookiefile
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.bitcoin_dir().join(".cookie"));
|
||||
let auth = if cookie.is_file() {
|
||||
Auth::CookieFile(cookie)
|
||||
} else if let (Some(u), Some(p)) = (self.rpcuser.as_deref(), self.rpcpassword.as_deref()) {
|
||||
Auth::UserPass(u.to_string(), p.to_string())
|
||||
} else {
|
||||
return Err(Error::Parse(
|
||||
"no RPC auth: cookie file missing and --rpcuser/--rpcpassword not set".into(),
|
||||
));
|
||||
};
|
||||
Client::new(&url, auth)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
use std::cell::OnceCell;
|
||||
|
||||
use bitcoin::{
|
||||
Address, Block, Network, ScriptBuf, Transaction, TxIn, TxOut, consensus::encode::serialize_hex,
|
||||
hex::DisplayHex,
|
||||
};
|
||||
use brk_error::{Error, Result};
|
||||
use brk_types::ReadBlock;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::path::{Path, Step};
|
||||
|
||||
pub struct Ctx<'a> {
|
||||
block: &'a ReadBlock,
|
||||
size_weight: OnceCell<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl<'a> Ctx<'a> {
|
||||
pub fn new(block: &'a ReadBlock) -> Self {
|
||||
Self {
|
||||
block,
|
||||
size_weight: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve(&self, path: &Path) -> Result<Value> {
|
||||
let (step, rest) = pop(&path.steps)?;
|
||||
let b = self.block;
|
||||
let raw: &Block = b;
|
||||
let scalar = |v| scalar_leaf(v, step, rest);
|
||||
match step.name.as_str() {
|
||||
"height" => scalar(json!(*b.height())),
|
||||
"hash" => scalar(json!(b.hash().to_string())),
|
||||
"time" => scalar(json!(b.header.time)),
|
||||
"version" => scalar(json!(b.header.version.to_consensus())),
|
||||
"version_hex" => scalar(json!(format!(
|
||||
"{:08x}",
|
||||
b.header.version.to_consensus() as u32
|
||||
))),
|
||||
"bits" => scalar(json!(b.header.bits.to_consensus())),
|
||||
"nonce" => scalar(json!(b.header.nonce)),
|
||||
"prev" => scalar(json!(b.header.prev_blockhash.to_string())),
|
||||
"merkle" => scalar(json!(b.header.merkle_root.to_string())),
|
||||
"difficulty" => scalar(json!(b.header.difficulty_float())),
|
||||
"txs" => scalar(json!(b.txdata.len())),
|
||||
"n_inputs" => scalar(json!(
|
||||
b.txdata.iter().map(|tx| tx.input.len()).sum::<usize>()
|
||||
)),
|
||||
"n_outputs" => scalar(json!(
|
||||
b.txdata.iter().map(|tx| tx.output.len()).sum::<usize>()
|
||||
)),
|
||||
"witness_txs" => scalar(json!(
|
||||
b.txdata.iter().filter(|tx| tx_has_witness(tx)).count()
|
||||
)),
|
||||
"size" => scalar(json!(self.size_and_weight().0)),
|
||||
"weight" => scalar(json!(self.size_and_weight().1)),
|
||||
"strippedsize" => {
|
||||
let (size, weight) = self.size_and_weight();
|
||||
scalar(json!((weight - size) / 3))
|
||||
}
|
||||
"subsidy" => scalar(json!(subsidy_sats(*b.height()))),
|
||||
"header_hex" => scalar(json!(serialize_hex(&b.header))),
|
||||
"hex" => scalar(json!(serialize_hex(raw))),
|
||||
"coinbase" => scalar(json!(b.coinbase_tag().as_str())),
|
||||
"tx" => pick(&b.txdata, step, rest, |i, tx| resolve_tx(tx, i == 0, rest)),
|
||||
other => Err(unknown("block", other)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_str(&self, path: &Path) -> Result<String> {
|
||||
Ok(match self.resolve(path)? {
|
||||
Value::String(s) => s,
|
||||
other => other.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn full(&self) -> Value {
|
||||
let b = self.block;
|
||||
let (size, weight) = self.size_and_weight();
|
||||
let tx: Vec<Value> = b
|
||||
.txdata
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, tx)| tx_to_value(tx, i == 0))
|
||||
.collect();
|
||||
json!({
|
||||
"height": *b.height(),
|
||||
"hash": b.hash().to_string(),
|
||||
"version": b.header.version.to_consensus(),
|
||||
"version_hex": format!("{:08x}", b.header.version.to_consensus() as u32),
|
||||
"merkle": b.header.merkle_root.to_string(),
|
||||
"time": b.header.time,
|
||||
"nonce": b.header.nonce,
|
||||
"bits": b.header.bits.to_consensus(),
|
||||
"difficulty": b.header.difficulty_float(),
|
||||
"prev": b.header.prev_blockhash.to_string(),
|
||||
"txs": b.txdata.len(),
|
||||
"n_inputs": b.txdata.iter().map(|t| t.input.len()).sum::<usize>(),
|
||||
"n_outputs": b.txdata.iter().map(|t| t.output.len()).sum::<usize>(),
|
||||
"witness_txs": b.txdata.iter().filter(|t| tx_has_witness(t)).count(),
|
||||
"size": size,
|
||||
"strippedsize": (weight - size) / 3,
|
||||
"weight": weight,
|
||||
"subsidy": subsidy_sats(*b.height()),
|
||||
"coinbase": b.coinbase_tag().as_str(),
|
||||
"header_hex": serialize_hex(&b.header),
|
||||
"tx": tx,
|
||||
})
|
||||
}
|
||||
|
||||
fn size_and_weight(&self) -> (usize, usize) {
|
||||
*self
|
||||
.size_weight
|
||||
.get_or_init(|| self.block.total_size_and_weight())
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_tx(tx: &Transaction, is_coinbase: bool, steps: &[Step]) -> Result<Value> {
|
||||
if steps.is_empty() {
|
||||
return Ok(tx_to_value(tx, is_coinbase));
|
||||
}
|
||||
let (step, rest) = pop(steps)?;
|
||||
let scalar = |v| scalar_leaf(v, step, rest);
|
||||
match step.name.as_str() {
|
||||
"txid" => scalar(json!(tx.compute_txid().to_string())),
|
||||
"wtxid" => scalar(json!(tx.compute_wtxid().to_string())),
|
||||
"version" => scalar(json!(tx.version.0)),
|
||||
"locktime" => scalar(json!(tx.lock_time.to_consensus_u32())),
|
||||
"size" => scalar(json!(tx.total_size())),
|
||||
"base_size" => scalar(json!(tx.base_size())),
|
||||
"vsize" => scalar(json!(tx.vsize())),
|
||||
"weight" => scalar(json!(tx.weight().to_wu())),
|
||||
"inputs" => scalar(json!(tx.input.len())),
|
||||
"outputs" => scalar(json!(tx.output.len())),
|
||||
"is_coinbase" => scalar(json!(is_coinbase)),
|
||||
"has_witness" => scalar(json!(tx_has_witness(tx))),
|
||||
"is_rbf" => scalar(json!(tx_is_rbf(tx))),
|
||||
"total_out" => scalar(json!(tx_total_out(tx))),
|
||||
"hex" => scalar(json!(serialize_hex(tx))),
|
||||
"vin" => pick(&tx.input, step, rest, |j, vin| {
|
||||
resolve_vin(vin, is_coinbase && j == 0, rest)
|
||||
}),
|
||||
"vout" => pick(&tx.output, step, rest, |_, vout| resolve_vout(vout, rest)),
|
||||
other => Err(unknown("tx", other)),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_vin(vin: &TxIn, is_coinbase: bool, steps: &[Step]) -> Result<Value> {
|
||||
if steps.is_empty() {
|
||||
return Ok(vin_to_value(vin, is_coinbase));
|
||||
}
|
||||
let (step, rest) = pop(steps)?;
|
||||
let scalar = |v| scalar_leaf(v, step, rest);
|
||||
match step.name.as_str() {
|
||||
"prev_txid" => scalar(json!(vin.previous_output.txid.to_string())),
|
||||
"prev_vout" => scalar(json!(vin.previous_output.vout)),
|
||||
"sequence" => scalar(json!(vin.sequence.0)),
|
||||
"script_sig" => scalar(json!(vin.script_sig.to_hex_string())),
|
||||
"script_sig_asm" => scalar(json!(vin.script_sig.to_asm_string())),
|
||||
"witness" => scalar(witness_to_value(vin)),
|
||||
"has_witness" => scalar(json!(!vin.witness.is_empty())),
|
||||
"is_rbf" => scalar(json!(vin.sequence.is_rbf())),
|
||||
"coinbase" => scalar(json!(is_coinbase)),
|
||||
other => Err(unknown("vin", other)),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_vout(vout: &TxOut, steps: &[Step]) -> Result<Value> {
|
||||
if steps.is_empty() {
|
||||
return Ok(vout_to_value(vout));
|
||||
}
|
||||
let (step, rest) = pop(steps)?;
|
||||
let scalar = |v| scalar_leaf(v, step, rest);
|
||||
match step.name.as_str() {
|
||||
"value" => scalar(json!(vout.value.to_sat())),
|
||||
"script_pubkey" => scalar(json!(vout.script_pubkey.to_hex_string())),
|
||||
"script_pubkey_asm" => scalar(json!(vout.script_pubkey.to_asm_string())),
|
||||
"type" => scalar(json!(script_type(&vout.script_pubkey))),
|
||||
"address" => scalar(address_value(&vout.script_pubkey)),
|
||||
other => Err(unknown("vout", other)),
|
||||
}
|
||||
}
|
||||
|
||||
fn pick<T>(
|
||||
items: &[T],
|
||||
step: &Step,
|
||||
_rest: &[Step],
|
||||
mut resolve: impl FnMut(usize, &T) -> Result<Value>,
|
||||
) -> Result<Value> {
|
||||
match step.index {
|
||||
Some(i) => {
|
||||
let item = items
|
||||
.get(i)
|
||||
.ok_or_else(|| out_of_range(&step.name, i, items.len()))?;
|
||||
resolve(i, item)
|
||||
}
|
||||
None => Ok(Value::Array(
|
||||
items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, item)| resolve(i, item))
|
||||
.collect::<Result<_>>()?,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn pop(steps: &[Step]) -> Result<(&Step, &[Step])> {
|
||||
steps
|
||||
.split_first()
|
||||
.ok_or_else(|| Error::Parse("empty path segment".into()))
|
||||
}
|
||||
|
||||
fn scalar_leaf(v: Value, step: &Step, rest: &[Step]) -> Result<Value> {
|
||||
if step.index.is_some() {
|
||||
return Err(Error::Parse(format!("'{}' is not an array", step.name)));
|
||||
}
|
||||
if !rest.is_empty() {
|
||||
return Err(Error::Parse(format!(
|
||||
"'{}' is a scalar; nothing to drill into",
|
||||
step.name
|
||||
)));
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn out_of_range(name: &str, i: usize, len: usize) -> Error {
|
||||
Error::Parse(format!("{name}.{i} out of range (len {len})"))
|
||||
}
|
||||
|
||||
fn unknown(level: &str, name: &str) -> Error {
|
||||
Error::Parse(format!(
|
||||
"unknown {level} field '{name}' (run `blk --help` for the list)"
|
||||
))
|
||||
}
|
||||
|
||||
fn tx_to_value(tx: &Transaction, is_coinbase: bool) -> Value {
|
||||
let vin: Vec<Value> = tx
|
||||
.input
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(j, v)| vin_to_value(v, is_coinbase && j == 0))
|
||||
.collect();
|
||||
let vout: Vec<Value> = tx.output.iter().map(vout_to_value).collect();
|
||||
json!({
|
||||
"txid": tx.compute_txid().to_string(),
|
||||
"wtxid": tx.compute_wtxid().to_string(),
|
||||
"version": tx.version.0,
|
||||
"locktime": tx.lock_time.to_consensus_u32(),
|
||||
"size": tx.total_size(),
|
||||
"base_size": tx.base_size(),
|
||||
"vsize": tx.vsize(),
|
||||
"weight": tx.weight().to_wu(),
|
||||
"inputs": tx.input.len(),
|
||||
"outputs": tx.output.len(),
|
||||
"is_coinbase": is_coinbase,
|
||||
"has_witness": tx_has_witness(tx),
|
||||
"is_rbf": tx_is_rbf(tx),
|
||||
"total_out": tx_total_out(tx),
|
||||
"hex": serialize_hex(tx),
|
||||
"vin": vin,
|
||||
"vout": vout,
|
||||
})
|
||||
}
|
||||
|
||||
fn vin_to_value(vin: &TxIn, is_coinbase: bool) -> Value {
|
||||
json!({
|
||||
"prev_txid": vin.previous_output.txid.to_string(),
|
||||
"prev_vout": vin.previous_output.vout,
|
||||
"sequence": vin.sequence.0,
|
||||
"script_sig": vin.script_sig.to_hex_string(),
|
||||
"script_sig_asm": vin.script_sig.to_asm_string(),
|
||||
"witness": witness_to_value(vin),
|
||||
"has_witness": !vin.witness.is_empty(),
|
||||
"is_rbf": vin.sequence.is_rbf(),
|
||||
"coinbase": is_coinbase,
|
||||
})
|
||||
}
|
||||
|
||||
fn vout_to_value(vout: &TxOut) -> Value {
|
||||
json!({
|
||||
"value": vout.value.to_sat(),
|
||||
"script_pubkey": vout.script_pubkey.to_hex_string(),
|
||||
"script_pubkey_asm": vout.script_pubkey.to_asm_string(),
|
||||
"type": script_type(&vout.script_pubkey),
|
||||
"address": address_value(&vout.script_pubkey),
|
||||
})
|
||||
}
|
||||
|
||||
fn tx_has_witness(tx: &Transaction) -> bool {
|
||||
tx.input.iter().any(|i| !i.witness.is_empty())
|
||||
}
|
||||
|
||||
fn tx_is_rbf(tx: &Transaction) -> bool {
|
||||
tx.input.iter().any(|i| i.sequence.is_rbf())
|
||||
}
|
||||
|
||||
fn tx_total_out(tx: &Transaction) -> u64 {
|
||||
tx.output.iter().map(|o| o.value.to_sat()).sum()
|
||||
}
|
||||
|
||||
fn subsidy_sats(height: u32) -> u64 {
|
||||
let halvings = height / 210_000;
|
||||
if halvings >= 64 {
|
||||
0
|
||||
} else {
|
||||
(50 * 100_000_000u64) >> halvings
|
||||
}
|
||||
}
|
||||
|
||||
fn witness_to_value(vin: &TxIn) -> Value {
|
||||
Value::Array(
|
||||
vin.witness
|
||||
.iter()
|
||||
.map(|w| Value::String(w.to_lower_hex_string()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn script_type(s: &ScriptBuf) -> &'static str {
|
||||
if s.is_p2pkh() {
|
||||
"p2pkh"
|
||||
} else if s.is_p2sh() {
|
||||
"p2sh"
|
||||
} else if s.is_p2wpkh() {
|
||||
"p2wpkh"
|
||||
} else if s.is_p2wsh() {
|
||||
"p2wsh"
|
||||
} else if s.is_p2tr() {
|
||||
"p2tr"
|
||||
} else if s.is_op_return() {
|
||||
"op_return"
|
||||
} else if s.is_p2pk() {
|
||||
"p2pk"
|
||||
} else {
|
||||
"unknown"
|
||||
}
|
||||
}
|
||||
|
||||
fn address_value(s: &ScriptBuf) -> Value {
|
||||
Address::from_script(s, Network::Bitcoin)
|
||||
.map(|a| Value::String(a.to_string()))
|
||||
.unwrap_or(Value::Null)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
use brk_error::Result;
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::{fields::Ctx, mode::Mode, path::Path};
|
||||
|
||||
pub struct Formatter {
|
||||
mode: Mode,
|
||||
fields: Vec<Path>,
|
||||
}
|
||||
|
||||
impl Formatter {
|
||||
pub fn new(mode: Mode, fields: Vec<Path>) -> Self {
|
||||
Self { mode, fields }
|
||||
}
|
||||
|
||||
pub fn format(&self, ctx: &Ctx) -> Result<String> {
|
||||
match self.mode {
|
||||
Mode::Bare => self.bare(ctx),
|
||||
Mode::Tsv => self.tsv(ctx),
|
||||
Mode::Json => Ok(serde_json::to_string(&self.object(ctx)?)?),
|
||||
Mode::Pretty => Ok(serde_json::to_string_pretty(&self.object(ctx)?)?),
|
||||
}
|
||||
}
|
||||
|
||||
fn bare(&self, ctx: &Ctx) -> Result<String> {
|
||||
Ok(match ctx.resolve(&self.fields[0])? {
|
||||
Value::String(s) => s,
|
||||
other => other.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn tsv(&self, ctx: &Ctx) -> Result<String> {
|
||||
let mut row = String::new();
|
||||
for (i, path) in self.fields.iter().enumerate() {
|
||||
if i > 0 {
|
||||
row.push('\t');
|
||||
}
|
||||
for c in ctx.resolve_str(path)?.chars() {
|
||||
row.push(if matches!(c, '\t' | '\n' | '\r') {
|
||||
' '
|
||||
} else {
|
||||
c
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
fn object(&self, ctx: &Ctx) -> Result<Value> {
|
||||
if self.fields.is_empty() {
|
||||
return Ok(ctx.full());
|
||||
}
|
||||
let mut obj = Map::with_capacity(self.fields.len());
|
||||
for path in &self.fields {
|
||||
obj.insert(path.raw.clone(), ctx.resolve(path)?);
|
||||
}
|
||||
Ok(Value::Object(obj))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
mod args;
|
||||
mod fields;
|
||||
mod formatter;
|
||||
mod mode;
|
||||
mod path;
|
||||
mod selector;
|
||||
mod usage;
|
||||
|
||||
use std::process::ExitCode;
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_reader::Reader;
|
||||
|
||||
use args::Args;
|
||||
use fields::Ctx;
|
||||
use formatter::Formatter;
|
||||
use mode::Mode;
|
||||
use selector::Selector;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match run() {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
eprintln!("blk: {e}");
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
let raw: Vec<String> = std::env::args().skip(1).collect();
|
||||
if raw.is_empty() || raw.iter().any(|a| matches!(a.as_str(), "-h" | "--help")) {
|
||||
usage::print();
|
||||
return Ok(());
|
||||
}
|
||||
let args = Args::parse(raw)?;
|
||||
|
||||
let client = args.rpc()?;
|
||||
let (start, end) = Selector::parse(&args.selector, &client)?;
|
||||
|
||||
let mode = Mode::pick(args.pretty, args.compact, args.paths.len());
|
||||
let reader = Reader::new(args.blocks_dir(), &client);
|
||||
let formatter = Formatter::new(mode, args.paths);
|
||||
let parser_threads = std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(2)
|
||||
/ 2;
|
||||
for block in reader.range_with(start, end, parser_threads)?.iter() {
|
||||
let block = block?;
|
||||
let line = formatter.format(&Ctx::new(&block))?;
|
||||
if !line.is_empty() {
|
||||
println!("{line}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Mode {
|
||||
Bare,
|
||||
Tsv,
|
||||
Json,
|
||||
Pretty,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
pub fn pick(pretty: bool, compact: bool, n_fields: usize) -> Self {
|
||||
if pretty {
|
||||
Self::Pretty
|
||||
} else if n_fields == 0 {
|
||||
Self::Json
|
||||
} else if n_fields == 1 {
|
||||
Self::Bare
|
||||
} else if compact {
|
||||
Self::Tsv
|
||||
} else {
|
||||
Self::Json
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use brk_error::{Error, Result};
|
||||
|
||||
pub struct Step {
|
||||
pub name: String,
|
||||
pub index: Option<usize>,
|
||||
}
|
||||
|
||||
pub struct Path {
|
||||
pub raw: String,
|
||||
pub steps: Vec<Step>,
|
||||
}
|
||||
|
||||
impl Path {
|
||||
pub fn parse(s: &str) -> Result<Self> {
|
||||
let parts: Vec<&str> = s.split('.').collect();
|
||||
let mut steps = Vec::new();
|
||||
let mut i = 0;
|
||||
while i < parts.len() {
|
||||
let name = parts[i];
|
||||
if name.is_empty() {
|
||||
return Err(Error::Parse(format!("bad path '{s}': empty segment")));
|
||||
}
|
||||
if name.parse::<usize>().is_ok() {
|
||||
return Err(Error::Parse(format!(
|
||||
"bad path '{s}': '{name}' must follow a field name"
|
||||
)));
|
||||
}
|
||||
let index = parts.get(i + 1).and_then(|p| p.parse::<usize>().ok());
|
||||
steps.push(Step {
|
||||
name: name.to_string(),
|
||||
index,
|
||||
});
|
||||
i += if index.is_some() { 2 } else { 1 };
|
||||
}
|
||||
Ok(Self {
|
||||
raw: s.to_string(),
|
||||
steps,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use brk_error::{Error, Result};
|
||||
use brk_rpc::Client;
|
||||
use brk_types::{CheckedSub, Height};
|
||||
|
||||
pub struct Selector;
|
||||
|
||||
impl Selector {
|
||||
pub fn parse(s: &str, client: &Client) -> Result<(Height, Height)> {
|
||||
let (start, end) = match s.split_once("..") {
|
||||
Some((a, b)) => (Self::endpoint(a, client)?, Self::endpoint(b, client)?),
|
||||
None => {
|
||||
let h = Self::endpoint(s, client)?;
|
||||
(h, h)
|
||||
}
|
||||
};
|
||||
if end < start {
|
||||
return Err(Error::Parse(format!(
|
||||
"range end {end} before start {start}"
|
||||
)));
|
||||
}
|
||||
Ok((start, end))
|
||||
}
|
||||
|
||||
fn endpoint(s: &str, client: &Client) -> Result<Height> {
|
||||
if s == "tip" {
|
||||
return client.get_last_height();
|
||||
}
|
||||
if let Some(rest) = s.strip_prefix("tip-") {
|
||||
let n: u32 = rest
|
||||
.parse()
|
||||
.map_err(|_| Error::Parse(format!("bad tip offset: {s}")))?;
|
||||
let tip = client.get_last_height()?;
|
||||
return tip
|
||||
.checked_sub(n)
|
||||
.ok_or_else(|| Error::Parse(format!("tip-{n} underflows genesis")));
|
||||
}
|
||||
let n: u32 = s
|
||||
.parse()
|
||||
.map_err(|_| Error::Parse(format!("bad height: {s}")))?;
|
||||
Ok(Height::new(n))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
use owo_colors::OwoColorize;
|
||||
|
||||
const SEL_W: usize = 5; // longest selector token: "tip-N"
|
||||
const LABEL_W: usize = 28; // longest label across OUTPUT/OPTIONS/EXAMPLES (= example cmd "blk 800000 tx.0.vout.0.value")
|
||||
const FLAG_W: usize = 15; // longest flag: "--rpccookiefile"
|
||||
const PH_W: usize = LABEL_W - FLAG_W - 1; // placeholder column width so flag+ph total = LABEL_W
|
||||
const GAP: usize = 4;
|
||||
|
||||
pub fn print() {
|
||||
println!("{} - inspect a Bitcoin Core block", "blk".bold());
|
||||
println!();
|
||||
|
||||
section("USAGE");
|
||||
println!(
|
||||
" blk {} [{} ...] [OPTIONS]",
|
||||
"<selector>".bright_black(),
|
||||
"<field>".bright_black()
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
"no fields = full block as JSON (analog of `bitcoin-cli getblock <hash> 2`)".bright_black()
|
||||
);
|
||||
println!();
|
||||
|
||||
section("SELECTOR");
|
||||
sel("<n>", "single height (e.g. 800000)");
|
||||
sel("tip", "current chain tip");
|
||||
sel("tip-N", "tip minus N");
|
||||
sel("a..b", "inclusive range, endpoints can be height/tip/tip-N");
|
||||
println!();
|
||||
|
||||
section("FIELDS");
|
||||
println!(
|
||||
" {}",
|
||||
"dotted paths drill into nested data; omit an index for arrays".bright_black()
|
||||
);
|
||||
println!();
|
||||
group("block");
|
||||
fields(&[
|
||||
"height, hash, time, version, version_hex, bits, nonce,",
|
||||
"prev, merkle, difficulty, txs, n_inputs, n_outputs,",
|
||||
"witness_txs, size, strippedsize, weight, subsidy, coinbase,",
|
||||
"header_hex, hex",
|
||||
]);
|
||||
println!();
|
||||
group_note("tx.i", "omit i for all txs");
|
||||
fields(&[
|
||||
"txid, wtxid, version, locktime, size, base_size, vsize,",
|
||||
"weight, inputs, outputs, is_coinbase, has_witness, is_rbf,",
|
||||
"total_out, hex",
|
||||
]);
|
||||
println!();
|
||||
group_note("tx.i.vin.j", "omit j for all inputs");
|
||||
fields(&[
|
||||
"prev_txid, prev_vout, sequence, script_sig, script_sig_asm,",
|
||||
"witness, has_witness, is_rbf, coinbase",
|
||||
]);
|
||||
println!();
|
||||
group_note("tx.i.vout.j", "omit j for all outputs");
|
||||
fields(&["value, script_pubkey, script_pubkey_asm, type, address"]);
|
||||
println!();
|
||||
println!(
|
||||
" {}",
|
||||
"Naked tx / tx.i / vin / vout returns the whole sub-object as JSON.".bright_black()
|
||||
);
|
||||
println!();
|
||||
|
||||
section("OUTPUT");
|
||||
out("no fields", "full block JSON object, one per line (NDJSON)");
|
||||
out("1 field", "bare value, one per line");
|
||||
out("2+ fields", "compact JSON object, one per line (NDJSON)");
|
||||
out("-p, --pretty", "pretty JSON object instead");
|
||||
out(
|
||||
"-c, --compact",
|
||||
"tab-separated values, no field names (TSV)",
|
||||
);
|
||||
println!();
|
||||
|
||||
section("OPTIONS");
|
||||
opt(
|
||||
"--bitcoindir",
|
||||
"<PATH>",
|
||||
"Bitcoin directory",
|
||||
Some("[OS default]"),
|
||||
);
|
||||
opt(
|
||||
"--blocksdir",
|
||||
"<PATH>",
|
||||
"Blocks directory",
|
||||
Some("[<bitcoindir>/blocks]"),
|
||||
);
|
||||
opt("--rpcconnect", "<IP>", "RPC host", Some("[localhost]"));
|
||||
opt("--rpcport", "<PORT>", "RPC port", Some("[8332]"));
|
||||
opt(
|
||||
"--rpccookiefile",
|
||||
"<PATH>",
|
||||
"RPC cookie file",
|
||||
Some("[<bitcoindir>/.cookie]"),
|
||||
);
|
||||
opt("--rpcuser", "<USERNAME>", "RPC username", None);
|
||||
opt("--rpcpassword", "<PASSWORD>", "RPC password", None);
|
||||
println!();
|
||||
|
||||
section("EXAMPLES");
|
||||
ex("blk 800000", "full block as JSON");
|
||||
ex("blk 800000 hash", "bare hash");
|
||||
ex("blk 800000 height hash time", "one compact JSON line");
|
||||
ex("blk 800000 tx.0.txid", "coinbase txid");
|
||||
ex("blk 800000 tx.txid", "all txids in block (array)");
|
||||
ex("blk 800000 tx.0.vout.0.value", "coinbase output 0 sats");
|
||||
ex("blk 800000 tx.0.vout.value", "all output sats for tx 0");
|
||||
ex("blk 800000 tx.vout.value", "array of arrays (per tx)");
|
||||
ex("blk 0..2 hash tx.0.txid", "3 NDJSON lines");
|
||||
ex("blk tip tx.0", "whole coinbase tx as JSON");
|
||||
}
|
||||
|
||||
fn section(name: &str) {
|
||||
println!("{}", format!("{name}:").bold());
|
||||
}
|
||||
|
||||
fn group(name: &str) {
|
||||
println!(" {}", format!("{name}:").bold());
|
||||
}
|
||||
|
||||
fn group_note(name: &str, note: &str) {
|
||||
println!(
|
||||
" {} {}",
|
||||
format!("{name}:").bold(),
|
||||
format!("({note})").bright_black()
|
||||
);
|
||||
}
|
||||
|
||||
fn fields(lines: &[&str]) {
|
||||
for line in lines {
|
||||
println!(" {line}");
|
||||
}
|
||||
}
|
||||
|
||||
fn pad(s: &str, width: usize) -> String {
|
||||
" ".repeat(width.saturating_sub(s.len()))
|
||||
}
|
||||
|
||||
fn sel(token: &str, desc: &str) {
|
||||
println!(
|
||||
" {}{}{}{desc}",
|
||||
token.bright_black(),
|
||||
pad(token, SEL_W),
|
||||
" ".repeat(GAP),
|
||||
);
|
||||
}
|
||||
|
||||
fn out(label: &str, desc: &str) {
|
||||
println!(
|
||||
" {label}{}{}{desc}",
|
||||
pad(label, LABEL_W),
|
||||
" ".repeat(GAP)
|
||||
);
|
||||
}
|
||||
|
||||
fn opt(flag: &str, ph: &str, desc: &str, default: Option<&str>) {
|
||||
let head = format!(
|
||||
" {flag}{} {}{}{}",
|
||||
pad(flag, FLAG_W),
|
||||
ph.bright_black(),
|
||||
pad(ph, PH_W),
|
||||
" ".repeat(GAP),
|
||||
);
|
||||
match default {
|
||||
Some(d) => println!("{head}{desc} {}", d.bright_black()),
|
||||
None => println!("{head}{desc}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn ex(cmd: &str, note: &str) {
|
||||
println!(
|
||||
" {cmd}{}{}{}",
|
||||
pad(cmd, LABEL_W),
|
||||
" ".repeat(GAP),
|
||||
format!("# {note}").bright_black()
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ full = [
|
||||
"iterator",
|
||||
"logger",
|
||||
"mempool",
|
||||
"oracle",
|
||||
"query",
|
||||
"reader",
|
||||
"rpc",
|
||||
@@ -39,6 +40,7 @@ indexer = ["brk_indexer"]
|
||||
iterator = ["brk_iterator"]
|
||||
logger = ["brk_logger"]
|
||||
mempool = ["brk_mempool"]
|
||||
oracle = ["brk_oracle"]
|
||||
query = ["brk_query"]
|
||||
reader = ["brk_reader"]
|
||||
rpc = ["brk_rpc"]
|
||||
@@ -59,6 +61,7 @@ brk_indexer = { workspace = true, optional = true }
|
||||
brk_iterator = { workspace = true, optional = true }
|
||||
brk_logger = { workspace = true, optional = true }
|
||||
brk_mempool = { workspace = true, optional = true }
|
||||
brk_oracle = { workspace = true, optional = true }
|
||||
brk_query = { workspace = true, optional = true }
|
||||
brk_reader = { workspace = true, optional = true }
|
||||
brk_rpc = { workspace = true, optional = true }
|
||||
|
||||
@@ -18,7 +18,12 @@ use brk::query::Query;
|
||||
use brk::types::Height;
|
||||
```
|
||||
|
||||
Feature flags match crate names without the `brk_` prefix. Use `full` to enable all.
|
||||
Feature flags match crate names without the `brk_` prefix. Use `full` to enable all:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
brk = { version = "0.1", features = ["full"] }
|
||||
```
|
||||
|
||||
## Crates
|
||||
|
||||
@@ -30,6 +35,7 @@ Feature flags match crate names without the `brk_` prefix. Use `full` to enable
|
||||
| [brk_indexer](https://docs.rs/brk_indexer) | Index transactions, addresses, and UTXOs |
|
||||
| [brk_computer](https://docs.rs/brk_computer) | Compute derived metrics (realized cap, MVRV, SOPR, cohorts, etc.) |
|
||||
| [brk_mempool](https://docs.rs/brk_mempool) | Monitor mempool, estimate fees, project upcoming blocks |
|
||||
| [brk_oracle](https://docs.rs/brk_oracle) | Pure on-chain BTC/USD price oracle |
|
||||
| [brk_query](https://docs.rs/brk_query) | Query interface for indexed and computed data |
|
||||
| [brk_server](https://docs.rs/brk_server) | REST API with OpenAPI docs |
|
||||
|
||||
|
||||
@@ -44,6 +44,10 @@ pub use brk_logger as logger;
|
||||
#[doc(inline)]
|
||||
pub use brk_mempool as mempool;
|
||||
|
||||
#[cfg(feature = "oracle")]
|
||||
#[doc(inline)]
|
||||
pub use brk_oracle as oracle;
|
||||
|
||||
#[cfg(feature = "query")]
|
||||
#[doc(inline)]
|
||||
pub use brk_query as query;
|
||||
|
||||
@@ -8,5 +8,5 @@ homepage.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
libmimalloc-sys = { version = "0.1.44", features = ["extended"] }
|
||||
mimalloc = { version = "0.1.48", features = ["v3"] }
|
||||
libmimalloc-sys = { version = "0.1.47", features = ["extended"] }
|
||||
mimalloc = { version = "0.1.50" }
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::{
|
||||
use std::fs;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use libproc::pid_rusage::{pidrusage, RUsageInfoV2};
|
||||
use libproc::pid_rusage::{RUsageInfoV2, pidrusage};
|
||||
|
||||
pub struct IoMonitor {
|
||||
pid: u32,
|
||||
|
||||
@@ -113,7 +113,9 @@ impl Bencher {
|
||||
self.0.stop_flag.store(true, Ordering::Relaxed);
|
||||
|
||||
if let Some(handle) = self.0.monitor_thread.lock().take() {
|
||||
handle.join().map_err(|_| Error::Internal("Monitor thread panicked"))??;
|
||||
handle
|
||||
.join()
|
||||
.map_err(|_| Error::Internal("Monitor thread panicked"))??;
|
||||
}
|
||||
|
||||
self.0.progression.flush()?;
|
||||
|
||||
@@ -43,14 +43,18 @@ pub fn generate(config: ChartConfig, runs: &[Run]) -> Result<()> {
|
||||
let max_value = runs.iter().map(|r| r.max_value()).fold(0.0, f64::max);
|
||||
|
||||
let (time_scaled, time_divisor, time_label) = format::time(max_time_s);
|
||||
let (value_scaled, scale_factor, y_label) = scale_y_axis(max_value, &config.y_label, &config.y_format);
|
||||
let (value_scaled, scale_factor, y_label) =
|
||||
scale_y_axis(max_value, &config.y_label, &config.y_format);
|
||||
let x_labels = label_count(time_scaled);
|
||||
|
||||
let root = SVGBackend::new(config.output_path, SIZE).into_drawing_area();
|
||||
root.fill(&BG_COLOR)?;
|
||||
|
||||
let mut chart = ChartBuilder::on(&root)
|
||||
.caption(&config.title, (FONT, FONT_SIZE_BIG).into_font().color(&TEXT_COLOR))
|
||||
.caption(
|
||||
&config.title,
|
||||
(FONT, FONT_SIZE_BIG).into_font().color(&TEXT_COLOR),
|
||||
)
|
||||
.margin(20)
|
||||
.margin_right(40)
|
||||
.x_label_area_size(50)
|
||||
@@ -62,7 +66,14 @@ pub fn generate(config: ChartConfig, runs: &[Run]) -> Result<()> {
|
||||
|
||||
for (idx, run) in runs.iter().enumerate() {
|
||||
let color = COLORS[idx % COLORS.len()];
|
||||
draw_series(&mut chart, &run.data, &run.id, color, time_divisor, scale_factor)?;
|
||||
draw_series(
|
||||
&mut chart,
|
||||
&run.data,
|
||||
&run.id,
|
||||
color,
|
||||
time_divisor,
|
||||
scale_factor,
|
||||
)?;
|
||||
}
|
||||
|
||||
configure_legend(&mut chart)?;
|
||||
@@ -93,14 +104,18 @@ pub fn generate_dual(
|
||||
let max_value = runs.iter().map(|r| r.max_value()).fold(0.0, f64::max);
|
||||
|
||||
let (time_scaled, time_divisor, time_label) = format::time(max_time_s);
|
||||
let (value_scaled, scale_factor, y_label) = scale_y_axis(max_value, &config.y_label, &config.y_format);
|
||||
let (value_scaled, scale_factor, y_label) =
|
||||
scale_y_axis(max_value, &config.y_label, &config.y_format);
|
||||
let x_labels = label_count(time_scaled);
|
||||
|
||||
let root = SVGBackend::new(config.output_path, SIZE).into_drawing_area();
|
||||
root.fill(&BG_COLOR)?;
|
||||
|
||||
let mut chart = ChartBuilder::on(&root)
|
||||
.caption(&config.title, (FONT, FONT_SIZE_BIG).into_font().color(&TEXT_COLOR))
|
||||
.caption(
|
||||
&config.title,
|
||||
(FONT, FONT_SIZE_BIG).into_font().color(&TEXT_COLOR),
|
||||
)
|
||||
.margin(20)
|
||||
.margin_right(40)
|
||||
.x_label_area_size(50)
|
||||
@@ -164,7 +179,13 @@ type Chart<'a, 'b> = ChartContext<
|
||||
Cartesian2d<plotters::coord::types::RangedCoordf64, plotters::coord::types::RangedCoordf64>,
|
||||
>;
|
||||
|
||||
fn configure_mesh(chart: &mut Chart, x_label: &str, y_label: &str, y_format: &YAxisFormat, x_labels: usize) -> Result<()> {
|
||||
fn configure_mesh(
|
||||
chart: &mut Chart,
|
||||
x_label: &str,
|
||||
y_label: &str,
|
||||
y_format: &YAxisFormat,
|
||||
x_labels: usize,
|
||||
) -> Result<()> {
|
||||
let y_formatter: Box<dyn Fn(&f64) -> String> = match y_format {
|
||||
YAxisFormat::Bytes => Box::new(|y: &f64| {
|
||||
if y.fract() == 0.0 {
|
||||
@@ -200,9 +221,12 @@ fn draw_series(
|
||||
time_divisor: f64,
|
||||
scale_factor: f64,
|
||||
) -> Result<()> {
|
||||
let points = data
|
||||
.iter()
|
||||
.map(|d| (d.timestamp_ms as f64 / 1000.0 / time_divisor, d.value / scale_factor));
|
||||
let points = data.iter().map(|d| {
|
||||
(
|
||||
d.timestamp_ms as f64 / 1000.0 / time_divisor,
|
||||
d.value / scale_factor,
|
||||
)
|
||||
});
|
||||
|
||||
chart
|
||||
.draw_series(LineSeries::new(points, color.stroke_width(1)))?
|
||||
@@ -221,7 +245,12 @@ fn draw_dashed_series(
|
||||
) -> Result<()> {
|
||||
let points: Vec<_> = data
|
||||
.iter()
|
||||
.map(|d| (d.timestamp_ms as f64 / 1000.0 / time_divisor, d.value / scale_factor))
|
||||
.map(|d| {
|
||||
(
|
||||
d.timestamp_ms as f64 / 1000.0 / time_divisor,
|
||||
d.value / scale_factor,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Draw dashed line by skipping every other segment
|
||||
@@ -234,7 +263,12 @@ fn draw_dashed_series(
|
||||
.map(|(_, w)| PathElement::new(vec![w[0], w[1]], color.stroke_width(2))),
|
||||
)?
|
||||
.label(label)
|
||||
.legend(move |(x, y)| PathElement::new(vec![(x, y), (x + 10, y), (x + 20, y)], color.stroke_width(2)));
|
||||
.legend(move |(x, y)| {
|
||||
PathElement::new(
|
||||
vec![(x, y), (x + 10, y), (x + 20, y)],
|
||||
color.stroke_width(2),
|
||||
)
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ mod chart;
|
||||
mod data;
|
||||
mod format;
|
||||
|
||||
use data::{read_dual_runs, read_runs, Cutoffs, DualRun, Result, Run};
|
||||
use data::{Cutoffs, DualRun, Result, Run, read_dual_runs, read_runs};
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
@@ -57,10 +57,24 @@ impl Visualizer {
|
||||
let io_runs = read_dual_runs(crate_path, "io.csv")?;
|
||||
|
||||
// Combined charts (all runs)
|
||||
self.generate_combined_charts(crate_path, crate_name, &disk_runs, &memory_runs, &progress_runs, &io_runs)?;
|
||||
self.generate_combined_charts(
|
||||
crate_path,
|
||||
crate_name,
|
||||
&disk_runs,
|
||||
&memory_runs,
|
||||
&progress_runs,
|
||||
&io_runs,
|
||||
)?;
|
||||
|
||||
// Individual charts (one per run)
|
||||
self.generate_individual_charts(crate_path, crate_name, &disk_runs, &memory_runs, &progress_runs, &io_runs)?;
|
||||
self.generate_individual_charts(
|
||||
crate_path,
|
||||
crate_name,
|
||||
&disk_runs,
|
||||
&memory_runs,
|
||||
&progress_runs,
|
||||
&io_runs,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -246,4 +260,3 @@ impl Visualizer {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ repository.workspace = true
|
||||
brk_cohort = { workspace = true }
|
||||
brk_query = { workspace = true }
|
||||
brk_types = { workspace = true }
|
||||
oas3 = "0.20"
|
||||
indexmap = { workspace = true }
|
||||
oas3 = "0.22"
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Common prefix/suffix detection for metric names.
|
||||
//! Common prefix/suffix detection for series names.
|
||||
//!
|
||||
//! This module provides utilities to find common prefixes and suffixes
|
||||
//! among metric names, which is used to detect pattern mode (suffix vs prefix).
|
||||
//! among series names, which is used to detect pattern mode (suffix vs prefix).
|
||||
|
||||
/// Find the longest common prefix among all strings.
|
||||
/// Returns the prefix WITH trailing underscore if found at word boundary.
|
||||
@@ -151,7 +151,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_common_prefix_lth() {
|
||||
let names = vec!["lth_cost_basis_max", "lth_cost_basis_min", "lth_cost_basis"];
|
||||
assert_eq!(find_common_prefix(&names), Some("lth_cost_basis_".to_string()));
|
||||
assert_eq!(
|
||||
find_common_prefix(&names),
|
||||
Some("lth_cost_basis_".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
//! This module detects repeating tree structures and analyzes them
|
||||
//! using the bottom-up name deconstruction algorithm.
|
||||
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
};
|
||||
|
||||
use brk_types::{TreeNode, extract_json_type};
|
||||
|
||||
@@ -13,25 +16,25 @@ use crate::{PatternBaseResult, PatternField, StructuralPattern, to_pascal_case};
|
||||
/// Context for pattern detection, holding all intermediate state.
|
||||
struct PatternContext {
|
||||
/// Maps field signatures to pattern names
|
||||
signature_to_pattern: HashMap<Vec<PatternField>, String>,
|
||||
signature_to_pattern: BTreeMap<Vec<PatternField>, String>,
|
||||
/// Counts how many times each signature appears
|
||||
signature_counts: HashMap<Vec<PatternField>, usize>,
|
||||
signature_counts: BTreeMap<Vec<PatternField>, usize>,
|
||||
/// Maps normalized signatures to pattern names (for naming consistency)
|
||||
normalized_to_name: HashMap<Vec<PatternField>, String>,
|
||||
normalized_to_name: BTreeMap<Vec<PatternField>, String>,
|
||||
/// Counts pattern name usage (for unique naming)
|
||||
name_counts: HashMap<String, usize>,
|
||||
name_counts: BTreeMap<String, usize>,
|
||||
/// Maps signatures to their child field lists
|
||||
signature_to_child_fields: HashMap<Vec<PatternField>, Vec<Vec<PatternField>>>,
|
||||
signature_to_child_fields: BTreeMap<Vec<PatternField>, Vec<Vec<PatternField>>>,
|
||||
}
|
||||
|
||||
impl PatternContext {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
signature_to_pattern: HashMap::new(),
|
||||
signature_counts: HashMap::new(),
|
||||
normalized_to_name: HashMap::new(),
|
||||
name_counts: HashMap::new(),
|
||||
signature_to_child_fields: HashMap::new(),
|
||||
signature_to_pattern: BTreeMap::new(),
|
||||
signature_counts: BTreeMap::new(),
|
||||
normalized_to_name: BTreeMap::new(),
|
||||
name_counts: BTreeMap::new(),
|
||||
signature_to_child_fields: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,12 +48,12 @@ pub fn detect_structural_patterns(
|
||||
tree: &TreeNode,
|
||||
) -> (
|
||||
Vec<StructuralPattern>,
|
||||
HashMap<Vec<PatternField>, String>,
|
||||
HashMap<Vec<PatternField>, String>,
|
||||
HashMap<String, PatternBaseResult>,
|
||||
BTreeMap<Vec<PatternField>, String>,
|
||||
BTreeMap<Vec<PatternField>, String>,
|
||||
BTreeMap<String, PatternBaseResult>,
|
||||
) {
|
||||
let mut ctx = PatternContext::new();
|
||||
resolve_branch_patterns(tree, "root", &mut ctx);
|
||||
resolve_branch_patterns(tree, &mut ctx);
|
||||
|
||||
let (generic_patterns, generic_mappings, type_mappings) =
|
||||
detect_generic_patterns(&ctx.signature_to_pattern);
|
||||
@@ -87,10 +90,17 @@ pub fn detect_structural_patterns(
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Deduplicate patterns by name - different signatures can map to the same name
|
||||
// when their normalized forms match but they can't be unified as generics
|
||||
{
|
||||
let mut seen_names: BTreeSet<String> = BTreeSet::new();
|
||||
patterns.retain(|p| seen_names.insert(p.name.clone()));
|
||||
}
|
||||
|
||||
patterns.extend(generic_patterns);
|
||||
|
||||
// Build pattern lookup for mode analysis (patterns appearing 2+ times)
|
||||
let mut pattern_lookup: HashMap<Vec<PatternField>, String> = HashMap::new();
|
||||
let mut pattern_lookup: BTreeMap<Vec<PatternField>, String> = BTreeMap::new();
|
||||
for (sig, name) in &ctx.signature_to_pattern {
|
||||
if ctx.signature_counts.get(sig).copied().unwrap_or(0) >= 2 {
|
||||
pattern_lookup.insert(sig.clone(), name.clone());
|
||||
@@ -104,35 +114,36 @@ pub fn detect_structural_patterns(
|
||||
// Also collects node bases for each tree path
|
||||
let node_bases = analyze_pattern_modes(tree, &mut patterns, &pattern_lookup);
|
||||
|
||||
patterns.sort_by(|a, b| b.fields.len().cmp(&a.fields.len()));
|
||||
patterns.sort_by_key(|p| Reverse(p.fields.len()));
|
||||
(patterns, concrete_to_pattern, type_mappings, node_bases)
|
||||
}
|
||||
|
||||
/// Detect generic patterns by grouping signatures by their normalized form.
|
||||
fn detect_generic_patterns(
|
||||
signature_to_pattern: &HashMap<Vec<PatternField>, String>,
|
||||
signature_to_pattern: &BTreeMap<Vec<PatternField>, String>,
|
||||
) -> (
|
||||
Vec<StructuralPattern>,
|
||||
HashMap<Vec<PatternField>, String>,
|
||||
HashMap<Vec<PatternField>, String>,
|
||||
BTreeMap<Vec<PatternField>, String>,
|
||||
BTreeMap<Vec<PatternField>, String>,
|
||||
) {
|
||||
let mut normalized_groups: HashMap<
|
||||
let mut normalized_groups: BTreeMap<
|
||||
Vec<PatternField>,
|
||||
Vec<(Vec<PatternField>, String, String)>,
|
||||
> = HashMap::new();
|
||||
> = BTreeMap::new();
|
||||
|
||||
for (fields, name) in signature_to_pattern {
|
||||
if let Some((normalized, extracted_type)) = normalize_fields_for_generic(fields) {
|
||||
normalized_groups
|
||||
.entry(normalized)
|
||||
.or_default()
|
||||
.push((fields.clone(), name.clone(), extracted_type));
|
||||
normalized_groups.entry(normalized).or_default().push((
|
||||
fields.clone(),
|
||||
name.clone(),
|
||||
extracted_type,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let mut patterns = Vec::new();
|
||||
let mut pattern_mappings: HashMap<Vec<PatternField>, String> = HashMap::new();
|
||||
let mut type_mappings: HashMap<Vec<PatternField>, String> = HashMap::new();
|
||||
let mut pattern_mappings: BTreeMap<Vec<PatternField>, String> = BTreeMap::new();
|
||||
let mut type_mappings: BTreeMap<Vec<PatternField>, String> = BTreeMap::new();
|
||||
|
||||
for (normalized_fields, group) in normalized_groups {
|
||||
if group.len() >= 2 {
|
||||
@@ -205,7 +216,10 @@ fn normalize_fields_for_generic(fields: &[PatternField]) -> Option<(Vec<PatternF
|
||||
// Only proceed if inner types differ from originals (meaning they had wrappers)
|
||||
// and all inner types are the same
|
||||
if inner_types.iter().all(|t| t == first_inner)
|
||||
&& inner_types.iter().zip(leaf_types.iter()).any(|(inner, orig)| inner != *orig)
|
||||
&& inner_types
|
||||
.iter()
|
||||
.zip(leaf_types.iter())
|
||||
.any(|(inner, orig)| inner != *orig)
|
||||
{
|
||||
let normalized = fields
|
||||
.iter()
|
||||
@@ -245,17 +259,19 @@ fn replace_inner_type(type_str: &str, replacement: &str) -> String {
|
||||
/// Recursively resolve branch patterns bottom-up.
|
||||
fn resolve_branch_patterns(
|
||||
node: &TreeNode,
|
||||
field_name: &str,
|
||||
ctx: &mut PatternContext,
|
||||
) -> Option<(String, Vec<PatternField>)> {
|
||||
let TreeNode::Branch(children) = node else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// Convert to sorted BTreeMap for consistent pattern detection
|
||||
let sorted_children: BTreeMap<_, _> = children.iter().collect();
|
||||
|
||||
let mut fields: Vec<PatternField> = Vec::new();
|
||||
let mut child_fields_vec: Vec<Vec<PatternField>> = Vec::new();
|
||||
|
||||
for (child_name, child_node) in children {
|
||||
for (child_name, child_node) in sorted_children {
|
||||
let (rust_type, json_type, indexes, child_fields) = match child_node {
|
||||
TreeNode::Leaf(leaf) => (
|
||||
leaf.kind().to_string(),
|
||||
@@ -264,9 +280,8 @@ fn resolve_branch_patterns(
|
||||
Vec::new(),
|
||||
),
|
||||
TreeNode::Branch(_) => {
|
||||
let (pattern_name, child_pattern_fields) =
|
||||
resolve_branch_patterns(child_node, child_name, ctx)
|
||||
.unwrap_or_else(|| ("Unknown".to_string(), Vec::new()));
|
||||
let (pattern_name, child_pattern_fields) = resolve_branch_patterns(child_node, ctx)
|
||||
.unwrap_or_else(|| ("Unknown".to_string(), Vec::new()));
|
||||
(
|
||||
pattern_name.clone(),
|
||||
pattern_name,
|
||||
@@ -285,7 +300,7 @@ fn resolve_branch_patterns(
|
||||
child_fields_vec.push(child_fields);
|
||||
}
|
||||
|
||||
fields.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
// Fields are already sorted since we iterated over BTreeMap
|
||||
*ctx.signature_counts.entry(fields.clone()).or_insert(0) += 1;
|
||||
|
||||
ctx.signature_to_child_fields
|
||||
@@ -296,12 +311,20 @@ fn resolve_branch_patterns(
|
||||
existing.clone()
|
||||
} else {
|
||||
let normalized = normalize_fields_for_naming(&fields);
|
||||
// Generate stable name from first word of each field (deduped, sorted)
|
||||
let first_words: BTreeSet<String> = fields
|
||||
.iter()
|
||||
.filter_map(|f| f.name.split('_').next())
|
||||
.map(to_pascal_case)
|
||||
.collect();
|
||||
let combined: String = first_words.into_iter().collect();
|
||||
let name = ctx
|
||||
.normalized_to_name
|
||||
.entry(normalized)
|
||||
.or_insert_with(|| generate_pattern_name(field_name, &mut ctx.name_counts))
|
||||
.or_insert_with(|| generate_pattern_name(&combined, &mut ctx.name_counts))
|
||||
.clone();
|
||||
ctx.signature_to_pattern.insert(fields.clone(), name.clone());
|
||||
ctx.signature_to_pattern
|
||||
.insert(fields.clone(), name.clone());
|
||||
name
|
||||
};
|
||||
|
||||
@@ -309,11 +332,21 @@ fn resolve_branch_patterns(
|
||||
}
|
||||
|
||||
/// Normalize fields for naming (same structure = same name).
|
||||
/// Only erases leaf types when all leaves share the same type — this ensures
|
||||
/// mixed-type signatures (e.g., StoredU32 raw + StoredU64 cumulative) get a
|
||||
/// different name than same-type signatures that can be genericized.
|
||||
fn normalize_fields_for_naming(fields: &[PatternField]) -> Vec<PatternField> {
|
||||
let leaf_types: Vec<&str> = fields
|
||||
.iter()
|
||||
.filter(|f| !f.is_branch())
|
||||
.map(|f| f.rust_type.as_str())
|
||||
.collect();
|
||||
let all_same = !leaf_types.is_empty() && leaf_types.iter().all(|t| *t == leaf_types[0]);
|
||||
|
||||
fields
|
||||
.iter()
|
||||
.map(|f| {
|
||||
if f.is_branch() {
|
||||
if f.is_branch() || !all_same {
|
||||
f.clone()
|
||||
} else {
|
||||
PatternField {
|
||||
@@ -329,7 +362,7 @@ fn normalize_fields_for_naming(fields: &[PatternField]) -> Vec<PatternField> {
|
||||
}
|
||||
|
||||
/// Generate a unique pattern name.
|
||||
fn generate_pattern_name(field_name: &str, name_counts: &mut HashMap<String, usize>) -> String {
|
||||
fn generate_pattern_name(field_name: &str, name_counts: &mut BTreeMap<String, usize>) -> String {
|
||||
let pascal = to_pascal_case(field_name);
|
||||
let sanitized = if pascal.chars().next().is_some_and(|c| c.is_ascii_digit()) {
|
||||
format!("_{}", pascal)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,9 +3,10 @@
|
||||
//! This module provides utilities for working with the TreeNode structure,
|
||||
//! including leaf name extraction and index pattern detection.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use brk_types::{Index, TreeNode, extract_json_type};
|
||||
use indexmap::IndexMap;
|
||||
|
||||
use crate::{IndexSetPattern, PatternField, child_type_name};
|
||||
|
||||
@@ -15,7 +16,7 @@ use super::{find_common_prefix, find_common_suffix, normalize_prefix};
|
||||
///
|
||||
/// This is useful for pattern base analysis where we want the "base" case
|
||||
/// (e.g., the leaf without suffix like `_btc` or `_usd`).
|
||||
fn get_shortest_leaf_name(node: &TreeNode) -> Option<String> {
|
||||
pub(super) fn get_shortest_leaf_name(node: &TreeNode) -> Option<String> {
|
||||
match node {
|
||||
TreeNode::Leaf(leaf) => Some(leaf.name().to_string()),
|
||||
TreeNode::Branch(children) => children
|
||||
@@ -26,9 +27,10 @@ fn get_shortest_leaf_name(node: &TreeNode) -> Option<String> {
|
||||
}
|
||||
|
||||
/// Get the field signature for a branch node's children.
|
||||
/// Fields are sorted alphabetically for consistent pattern matching.
|
||||
pub fn get_node_fields(
|
||||
children: &BTreeMap<String, TreeNode>,
|
||||
pattern_lookup: &HashMap<Vec<PatternField>, String>,
|
||||
children: &IndexMap<String, TreeNode>,
|
||||
pattern_lookup: &BTreeMap<Vec<PatternField>, String>,
|
||||
) -> Vec<PatternField> {
|
||||
let mut fields: Vec<PatternField> = children
|
||||
.iter()
|
||||
@@ -57,11 +59,12 @@ pub fn get_node_fields(
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
// Sort for consistent pattern matching (display order preserved in IndexMap)
|
||||
fields.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
fields
|
||||
}
|
||||
|
||||
/// Detect index patterns (sets of indexes that appear together on metrics).
|
||||
/// Detect index patterns (sets of indexes that appear together on series).
|
||||
pub fn detect_index_patterns(tree: &TreeNode) -> Vec<IndexSetPattern> {
|
||||
let mut unique_index_sets: BTreeSet<BTreeSet<Index>> = BTreeSet::new();
|
||||
collect_index_sets_from_tree(tree, &mut unique_index_sets);
|
||||
@@ -82,7 +85,7 @@ pub fn detect_index_patterns(tree: &TreeNode) -> Vec<IndexSetPattern> {
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, indexes)| IndexSetPattern {
|
||||
name: format!("MetricPattern{}", i + 1),
|
||||
name: format!("SeriesPattern{}", i + 1),
|
||||
indexes,
|
||||
})
|
||||
.collect()
|
||||
@@ -117,7 +120,7 @@ pub struct PatternBaseResult {
|
||||
pub is_suffix_mode: bool,
|
||||
/// The field parts (suffix in suffix mode, prefix in prefix mode) for each field.
|
||||
/// Used to check if instance field parts match the pattern's field parts.
|
||||
pub field_parts: HashMap<String, String>,
|
||||
pub field_parts: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl PatternBaseResult {
|
||||
@@ -128,7 +131,7 @@ impl PatternBaseResult {
|
||||
base: String::new(),
|
||||
has_outlier: true,
|
||||
is_suffix_mode: true,
|
||||
field_parts: HashMap::new(),
|
||||
field_parts: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,12 +142,12 @@ impl PatternBaseResult {
|
||||
base: String::new(),
|
||||
has_outlier: false,
|
||||
is_suffix_mode: true,
|
||||
field_parts: HashMap::new(),
|
||||
field_parts: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the metric base for a pattern instance by analyzing direct children.
|
||||
/// Get the series base for a pattern instance by analyzing direct children.
|
||||
///
|
||||
/// Uses the shortest leaf names from direct children to find common prefix/suffix.
|
||||
///
|
||||
@@ -191,7 +194,7 @@ pub fn get_pattern_instance_base(node: &TreeNode) -> PatternBaseResult {
|
||||
}
|
||||
|
||||
// Fallback: no common prefix/suffix found - this is a root-level pattern
|
||||
// Return empty base so metric names are used directly
|
||||
// Return empty base so series names are used directly
|
||||
PatternBaseResult::empty()
|
||||
}
|
||||
|
||||
@@ -200,7 +203,7 @@ struct FindBaseResult {
|
||||
base: String,
|
||||
has_outlier: bool,
|
||||
is_suffix_mode: bool,
|
||||
field_parts: HashMap<String, String>,
|
||||
field_parts: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
/// Try to find a common base from child names using prefix/suffix detection.
|
||||
@@ -214,7 +217,7 @@ fn try_find_base(
|
||||
// Try common prefix first (suffix mode)
|
||||
if let Some(prefix) = find_common_prefix(&leaf_names) {
|
||||
let base = prefix.trim_end_matches('_').to_string();
|
||||
let mut field_parts = HashMap::new();
|
||||
let mut field_parts = BTreeMap::new();
|
||||
for (field_name, leaf_name) in child_names {
|
||||
// Compute the suffix part for this field
|
||||
let suffix = if leaf_name == &base {
|
||||
@@ -238,7 +241,7 @@ fn try_find_base(
|
||||
// Try common suffix (prefix mode)
|
||||
if let Some(suffix) = find_common_suffix(&leaf_names) {
|
||||
let base = suffix.trim_start_matches('_').to_string();
|
||||
let mut field_parts = HashMap::new();
|
||||
let mut field_parts = BTreeMap::new();
|
||||
for (field_name, leaf_name) in child_names {
|
||||
// Compute the prefix part for this field, normalized to end with _
|
||||
let prefix_part = leaf_name
|
||||
@@ -298,9 +301,9 @@ pub fn infer_accumulated_name(parent_acc: &str, field_name: &str, descendant_lea
|
||||
|
||||
/// Get fields with child field information for generic pattern lookup.
|
||||
pub fn get_fields_with_child_info(
|
||||
children: &BTreeMap<String, TreeNode>,
|
||||
children: &IndexMap<String, TreeNode>,
|
||||
parent_name: &str,
|
||||
pattern_lookup: &HashMap<Vec<PatternField>, String>,
|
||||
pattern_lookup: &BTreeMap<Vec<PatternField>, String>,
|
||||
) -> Vec<(PatternField, Option<Vec<PatternField>>)> {
|
||||
children
|
||||
.iter()
|
||||
@@ -343,20 +346,19 @@ pub fn get_fields_with_child_info(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use brk_types::{MetricLeaf, MetricLeafWithSchema, TreeNode};
|
||||
use std::collections::BTreeMap;
|
||||
use brk_types::{SeriesLeaf, SeriesLeafWithSchema, TreeNode};
|
||||
|
||||
fn make_leaf(name: &str) -> TreeNode {
|
||||
let leaf = MetricLeaf {
|
||||
let leaf = SeriesLeaf {
|
||||
name: name.to_string(),
|
||||
kind: "TestType".to_string(),
|
||||
indexes: BTreeSet::new(),
|
||||
};
|
||||
TreeNode::Leaf(MetricLeafWithSchema::new(leaf, serde_json::json!({})))
|
||||
TreeNode::Leaf(SeriesLeafWithSchema::new(leaf, serde_json::json!({})))
|
||||
}
|
||||
|
||||
fn make_branch(children: Vec<(&str, TreeNode)>) -> TreeNode {
|
||||
let map: BTreeMap<String, TreeNode> = children
|
||||
let map: IndexMap<String, TreeNode> = children
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_string(), v))
|
||||
.collect();
|
||||
@@ -369,15 +371,15 @@ mod tests {
|
||||
let tree = make_branch(vec![
|
||||
(
|
||||
"base",
|
||||
make_branch(vec![("dateindex", make_leaf("block_vbytes"))]),
|
||||
make_branch(vec![("day1", make_leaf("block_vbytes"))]),
|
||||
),
|
||||
(
|
||||
"average",
|
||||
make_branch(vec![("dateindex", make_leaf("block_vbytes_average"))]),
|
||||
make_branch(vec![("day1", make_leaf("block_vbytes_average"))]),
|
||||
),
|
||||
(
|
||||
"sum",
|
||||
make_branch(vec![("dateindex", make_leaf("block_vbytes_sum"))]),
|
||||
make_branch(vec![("day1", make_leaf("block_vbytes_sum"))]),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -388,27 +390,27 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_get_pattern_instance_base_without_base_field() {
|
||||
// Simulates weight tree: NO base field, only suffixed metrics
|
||||
// Simulates weight tree: NO base field, only suffixed series
|
||||
let tree = make_branch(vec![
|
||||
(
|
||||
"average",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_average"))]),
|
||||
make_branch(vec![("day1", make_leaf("block_weight_average"))]),
|
||||
),
|
||||
(
|
||||
"sum",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_sum"))]),
|
||||
make_branch(vec![("day1", make_leaf("block_weight_sum"))]),
|
||||
),
|
||||
(
|
||||
"cumulative",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_cumulative"))]),
|
||||
make_branch(vec![("day1", make_leaf("block_weight_cumulative"))]),
|
||||
),
|
||||
(
|
||||
"max",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_max"))]),
|
||||
make_branch(vec![("day1", make_leaf("block_weight_max"))]),
|
||||
),
|
||||
(
|
||||
"min",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_min"))]),
|
||||
make_branch(vec![("day1", make_leaf("block_weight_min"))]),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -424,15 +426,15 @@ mod tests {
|
||||
let tree = make_branch(vec![
|
||||
(
|
||||
"base",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_average"))]),
|
||||
make_branch(vec![("day1", make_leaf("block_weight_average"))]),
|
||||
),
|
||||
(
|
||||
"average",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_average"))]),
|
||||
make_branch(vec![("day1", make_leaf("block_weight_average"))]),
|
||||
),
|
||||
(
|
||||
"sum",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_sum"))]),
|
||||
make_branch(vec![("day1", make_leaf("block_weight_sum"))]),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -445,7 +447,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_get_pattern_instance_base_with_mismatched_base_name() {
|
||||
// Simulates the actual bug: indexed tree's "base" field has name "weight"
|
||||
// but computed tree's derived metrics use "block_weight_*" prefix.
|
||||
// but computed tree's derived series use "block_weight_*" prefix.
|
||||
// After tree merge, we get a base field with mismatched naming.
|
||||
let tree = make_branch(vec![
|
||||
("base", make_leaf("weight")), // Outlier - doesn't match pattern
|
||||
@@ -464,11 +466,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_get_pattern_instance_base_root_level_no_common_pattern() {
|
||||
// Simulates root-level pattern with metrics that have no common prefix/suffix.
|
||||
// Simulates root-level pattern with series that have no common prefix/suffix.
|
||||
// These names have no shared prefix or suffix, even when excluding any one.
|
||||
// In this case, we should return empty base so metric names are used directly.
|
||||
// In this case, we should return empty base so series names are used directly.
|
||||
let tree = make_branch(vec![
|
||||
("alpha", make_leaf("foo_metric")),
|
||||
("alpha", make_leaf("foo_series")),
|
||||
("beta", make_leaf("bar_value")),
|
||||
("gamma", make_leaf("baz_count")),
|
||||
]);
|
||||
@@ -511,10 +513,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_get_pattern_instance_base_suffix_mode_price_ago() {
|
||||
// Simulates price_ago pattern: price_1d_ago, price_1w_ago, price_10y_ago
|
||||
// Simulates price_ago pattern: price_24h_ago, price_1w_ago, price_10y_ago
|
||||
// Common prefix is "price_", so this is suffix mode
|
||||
let tree = make_branch(vec![
|
||||
("_1d", make_leaf("price_1d_ago")),
|
||||
("_24h", make_leaf("price_24h_ago")),
|
||||
("_1w", make_leaf("price_1w_ago")),
|
||||
("_1m", make_leaf("price_1m_ago")),
|
||||
("_10y", make_leaf("price_10y_ago")),
|
||||
@@ -522,16 +524,16 @@ mod tests {
|
||||
|
||||
let result = get_pattern_instance_base(&tree);
|
||||
assert_eq!(result.base, "price");
|
||||
assert!(result.is_suffix_mode); // Suffix mode: _m(base, "1d_ago")
|
||||
assert!(result.is_suffix_mode); // Suffix mode: _m(base, "24h_ago")
|
||||
assert!(!result.has_outlier);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_pattern_instance_base_prefix_mode_price_returns() {
|
||||
// Simulates price_returns pattern: 1d_price_returns, 1w_price_returns, 10y_price_returns
|
||||
// Simulates price_returns pattern: 24h_price_returns, 1w_price_returns, 10y_price_returns
|
||||
// Common suffix is "_price_returns", so this is prefix mode
|
||||
let tree = make_branch(vec![
|
||||
("_1d", make_leaf("1d_price_returns")),
|
||||
("_24h", make_leaf("24h_price_returns")),
|
||||
("_1w", make_leaf("1w_price_returns")),
|
||||
("_1m", make_leaf("1m_price_returns")),
|
||||
("_10y", make_leaf("10y_price_returns")),
|
||||
@@ -539,7 +541,7 @@ mod tests {
|
||||
|
||||
let result = get_pattern_instance_base(&tree);
|
||||
assert_eq!(result.base, "price_returns");
|
||||
assert!(!result.is_suffix_mode); // Prefix mode: _p("1d_", base)
|
||||
assert!(!result.is_suffix_mode); // Prefix mode: _p("24h_", base)
|
||||
assert!(!result.has_outlier);
|
||||
}
|
||||
|
||||
|
||||
@@ -59,4 +59,51 @@ impl LanguageSyntax for JavaScriptSyntax {
|
||||
fn constructor_name(&self, type_name: &str) -> String {
|
||||
format!("create{}", type_name)
|
||||
}
|
||||
|
||||
fn disc_arg_expr(&self, template: &str) -> String {
|
||||
if template == "{disc}" {
|
||||
"disc".to_string()
|
||||
} else if template.is_empty() {
|
||||
"''".to_string()
|
||||
} else if !template.contains("{disc}") {
|
||||
format!("'{}'", template)
|
||||
} else if template.ends_with("{disc}") {
|
||||
let static_part = template.trim_end_matches("{disc}").trim_end_matches('_');
|
||||
format!("_m('{}', disc)", static_part)
|
||||
} else {
|
||||
let js_template = template.replace("{disc}", "${disc}");
|
||||
format!("`{}`", js_template)
|
||||
}
|
||||
}
|
||||
|
||||
fn template_expr(&self, acc_var: &str, template: &str) -> String {
|
||||
let var_name = to_camel_case(acc_var);
|
||||
if template.is_empty() {
|
||||
// Identity — just pass disc
|
||||
format!("_m({}, disc)", var_name)
|
||||
} else if template == "{disc}" {
|
||||
// Template IS the discriminator
|
||||
format!("_m({}, disc)", var_name)
|
||||
} else if !template.contains("{disc}") {
|
||||
// Static suffix — no disc involved
|
||||
format!("_m({}, '{}')", var_name, template)
|
||||
} else {
|
||||
// Template with {disc}: use nested _m for proper separator handling
|
||||
// "ratio_{disc}_bps" → split on {disc} → _m(_m(acc, 'ratio'), disc) then _bps
|
||||
// But this is complex. For embedded disc, use string interpolation.
|
||||
// For suffix disc (ends with {disc}), use _m composition.
|
||||
if let Some(static_part) = template.strip_suffix("{disc}") {
|
||||
if static_part.is_empty() {
|
||||
format!("_m({}, disc)", var_name)
|
||||
} else {
|
||||
let static_part = static_part.trim_end_matches('_');
|
||||
format!("_m(_m({}, '{}'), disc)", var_name, static_part)
|
||||
}
|
||||
} else {
|
||||
// Embedded disc — use template literal
|
||||
let js_template = template.replace("{disc}", "${disc}");
|
||||
format!("_m({}, `{}`)", var_name, js_template)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,4 +54,29 @@ impl LanguageSyntax for PythonSyntax {
|
||||
fn constructor_name(&self, type_name: &str) -> String {
|
||||
type_name.to_string()
|
||||
}
|
||||
|
||||
fn disc_arg_expr(&self, template: &str) -> String {
|
||||
if template == "{disc}" {
|
||||
"disc".to_string()
|
||||
} else if template.is_empty() {
|
||||
"''".to_string()
|
||||
} else if !template.contains("{disc}") {
|
||||
format!("'{}'", template)
|
||||
} else if template.ends_with("{disc}") {
|
||||
let static_part = template.trim_end_matches("{disc}").trim_end_matches('_');
|
||||
format!("_m('{}', disc)", static_part)
|
||||
} else {
|
||||
format!("f'{}'", template)
|
||||
}
|
||||
}
|
||||
|
||||
fn template_expr(&self, acc_var: &str, template: &str) -> String {
|
||||
if template == "{disc}" {
|
||||
format!("_m({}, disc)", acc_var)
|
||||
} else if !template.contains("{disc}") {
|
||||
format!("_m({}, '{}')", acc_var, template)
|
||||
} else {
|
||||
format!("_m({}, f'{}')", acc_var, template)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
//! Rust language syntax implementation.
|
||||
|
||||
use crate::{GenericSyntax, LanguageSyntax, to_snake_case};
|
||||
use crate::{GenericSyntax, LanguageSyntax, escape_rust_keyword, to_snake_case};
|
||||
|
||||
/// Rust-specific code generation syntax.
|
||||
pub struct RustSyntax;
|
||||
|
||||
/// Escape braces in a template string for use in `format!()`, preserving `{disc}`.
|
||||
fn escape_rust_format(template: &str) -> String {
|
||||
template
|
||||
.replace('{', "{{")
|
||||
.replace('}', "}}")
|
||||
.replace("{{disc}}", "{disc}")
|
||||
}
|
||||
|
||||
impl LanguageSyntax for RustSyntax {
|
||||
fn field_name(&self, name: &str) -> String {
|
||||
to_snake_case(name)
|
||||
escape_rust_keyword(&to_snake_case(name))
|
||||
}
|
||||
|
||||
fn path_expr(&self, base_var: &str, suffix: &str) -> String {
|
||||
@@ -16,20 +24,20 @@ impl LanguageSyntax for RustSyntax {
|
||||
|
||||
fn suffix_expr(&self, acc_var: &str, relative: &str) -> String {
|
||||
if relative.is_empty() {
|
||||
// Identity: just return acc
|
||||
format!("{}.clone()", acc_var)
|
||||
self.owned_expr(acc_var)
|
||||
} else {
|
||||
// _m(&acc, relative) -> if acc.is_empty() { relative } else { format!("{acc}_{relative}") }
|
||||
format!("_m(&{}, \"{}\")", acc_var, relative)
|
||||
}
|
||||
}
|
||||
|
||||
fn owned_expr(&self, var: &str) -> String {
|
||||
format!("{}.clone()", var)
|
||||
}
|
||||
|
||||
fn prefix_expr(&self, prefix: &str, acc_var: &str) -> String {
|
||||
if prefix.is_empty() {
|
||||
// Identity: just return acc
|
||||
format!("{}.clone()", acc_var)
|
||||
self.owned_expr(acc_var)
|
||||
} else {
|
||||
// _p(prefix, &acc) -> if acc.is_empty() { prefix_base } else { format!("{prefix}{acc}") }
|
||||
let prefix_base = prefix.trim_end_matches('_');
|
||||
format!("_p(\"{}\", &{})", prefix_base, acc_var)
|
||||
}
|
||||
@@ -55,4 +63,35 @@ impl LanguageSyntax for RustSyntax {
|
||||
fn constructor_name(&self, type_name: &str) -> String {
|
||||
format!("{}::new", type_name)
|
||||
}
|
||||
|
||||
fn disc_arg_expr(&self, template: &str) -> String {
|
||||
if template == "{disc}" {
|
||||
"disc.clone()".to_string()
|
||||
} else if template.is_empty() {
|
||||
"String::new()".to_string()
|
||||
} else if !template.contains("{disc}") {
|
||||
format!("\"{}\".to_string()", template)
|
||||
} else if template.ends_with("{disc}") {
|
||||
let static_part = template.trim_end_matches("{disc}").trim_end_matches('_');
|
||||
format!("_m(\"{}\", &disc)", static_part)
|
||||
} else {
|
||||
format!("format!(\"{}\")", escape_rust_format(template))
|
||||
}
|
||||
}
|
||||
|
||||
fn template_expr(&self, acc_var: &str, template: &str) -> String {
|
||||
if template == "{disc}" {
|
||||
format!("_m(&{}, &disc)", acc_var)
|
||||
} else if template.is_empty() {
|
||||
acc_var.to_string()
|
||||
} else if !template.contains("{disc}") {
|
||||
format!("_m(&{}, \"{}\")", acc_var, template)
|
||||
} else {
|
||||
format!(
|
||||
"_m(&{}, &format!(\"{}\", disc=disc))",
|
||||
acc_var,
|
||||
escape_rust_format(template)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,33 +6,36 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use brk_cohort::{
|
||||
AGE_RANGE_NAMES, AMOUNT_RANGE_NAMES, EPOCH_NAMES, GE_AMOUNT_NAMES, LT_AMOUNT_NAMES,
|
||||
MAX_AGE_NAMES, MIN_AGE_NAMES, SPENDABLE_TYPE_NAMES, TERM_NAMES, YEAR_NAMES,
|
||||
AGE_RANGE_NAMES, AMOUNT_RANGE_NAMES, CLASS_NAMES, EPOCH_NAMES, LOSS_NAMES, OVER_AGE_NAMES,
|
||||
OVER_AMOUNT_NAMES, PROFIT_NAMES, PROFITABILITY_RANGE_NAMES, SPENDABLE_TYPE_NAMES, TERM_NAMES,
|
||||
UNDER_AGE_NAMES, UNDER_AMOUNT_NAMES,
|
||||
};
|
||||
use brk_types::{pools, Index, PoolSlug};
|
||||
use brk_types::{Index, pools};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{to_camel_case, VERSION};
|
||||
use crate::{VERSION, to_camel_case};
|
||||
|
||||
/// Collected constant data for client generation.
|
||||
pub struct ClientConstants {
|
||||
pub version: String,
|
||||
pub indexes: Vec<&'static str>,
|
||||
pub pool_map: BTreeMap<PoolSlug, &'static str>,
|
||||
pub pool_map: BTreeMap<String, &'static str>,
|
||||
}
|
||||
|
||||
impl ClientConstants {
|
||||
/// Collect all constant data.
|
||||
pub fn collect() -> Self {
|
||||
let indexes = Index::all();
|
||||
let indexes: Vec<&'static str> = indexes.iter().map(|i| i.serialize_long()).collect();
|
||||
let indexes: Vec<&'static str> = indexes.iter().map(|i| i.name()).collect();
|
||||
|
||||
let pools = pools();
|
||||
let mut sorted_pools: Vec<_> = pools.iter().collect();
|
||||
sorted_pools.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
||||
let pool_map: BTreeMap<PoolSlug, &'static str> =
|
||||
sorted_pools.iter().map(|p| (p.slug(), p.name)).collect();
|
||||
sorted_pools.sort_by_key(|p| p.name.to_lowercase());
|
||||
let pool_map: BTreeMap<String, &'static str> = sorted_pools
|
||||
.iter()
|
||||
.map(|p| (p.slug().to_string(), p.name))
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
version: format!("v{}", VERSION),
|
||||
@@ -55,14 +58,20 @@ impl CohortConstants {
|
||||
vec![
|
||||
("TERM_NAMES", to_value(&TERM_NAMES)),
|
||||
("EPOCH_NAMES", to_value(&EPOCH_NAMES)),
|
||||
("YEAR_NAMES", to_value(&YEAR_NAMES)),
|
||||
("CLASS_NAMES", to_value(&CLASS_NAMES)),
|
||||
("SPENDABLE_TYPE_NAMES", to_value(&SPENDABLE_TYPE_NAMES)),
|
||||
("AGE_RANGE_NAMES", to_value(&AGE_RANGE_NAMES)),
|
||||
("MAX_AGE_NAMES", to_value(&MAX_AGE_NAMES)),
|
||||
("MIN_AGE_NAMES", to_value(&MIN_AGE_NAMES)),
|
||||
("UNDER_AGE_NAMES", to_value(&UNDER_AGE_NAMES)),
|
||||
("OVER_AGE_NAMES", to_value(&OVER_AGE_NAMES)),
|
||||
("AMOUNT_RANGE_NAMES", to_value(&AMOUNT_RANGE_NAMES)),
|
||||
("GE_AMOUNT_NAMES", to_value(&GE_AMOUNT_NAMES)),
|
||||
("LT_AMOUNT_NAMES", to_value(<_AMOUNT_NAMES)),
|
||||
("OVER_AMOUNT_NAMES", to_value(&OVER_AMOUNT_NAMES)),
|
||||
("UNDER_AMOUNT_NAMES", to_value(&UNDER_AMOUNT_NAMES)),
|
||||
(
|
||||
"PROFITABILITY_RANGE_NAMES",
|
||||
to_value(&PROFITABILITY_RANGE_NAMES),
|
||||
),
|
||||
("PROFIT_NAMES", to_value(&PROFIT_NAMES)),
|
||||
("LOSS_NAMES", to_value(&LOSS_NAMES)),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
use brk_types::MetricLeafWithSchema;
|
||||
use brk_types::SeriesLeafWithSchema;
|
||||
|
||||
use crate::{ClientMetadata, LanguageSyntax, PatternField, StructuralPattern};
|
||||
use crate::{
|
||||
ClientMetadata, LanguageSyntax, PatternBaseResult, PatternField, PatternMode, StructuralPattern,
|
||||
};
|
||||
|
||||
/// Create a path suffix from a name.
|
||||
/// Adds `_` prefix only if the name doesn't already start with `_`.
|
||||
fn path_suffix(name: &str) -> String {
|
||||
if name.starts_with('_') {
|
||||
name.to_string()
|
||||
@@ -20,50 +21,52 @@ fn path_suffix(name: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute path expression from pattern mode and field part.
|
||||
fn compute_path_expr<S: LanguageSyntax>(
|
||||
/// Compute the constructor value for a parameterized field (factory context).
|
||||
///
|
||||
/// Handles all three pattern modes (Suffix/Prefix/Templated) and the special
|
||||
/// case of templated child patterns that need (acc, disc) instead of a path.
|
||||
fn compute_parameterized_value<S: LanguageSyntax>(
|
||||
syntax: &S,
|
||||
field: &PatternField,
|
||||
pattern: &StructuralPattern,
|
||||
field: &PatternField,
|
||||
base_var: &str,
|
||||
) -> String {
|
||||
match pattern.get_field_part(&field.name) {
|
||||
Some(part) => {
|
||||
if pattern.is_suffix_mode() {
|
||||
syntax.suffix_expr(base_var, part)
|
||||
} else {
|
||||
syntax.prefix_expr(part, base_var)
|
||||
}
|
||||
}
|
||||
None => syntax.path_expr(base_var, &path_suffix(&field.name)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute field value from path expression.
|
||||
fn compute_field_value<S: LanguageSyntax>(
|
||||
syntax: &S,
|
||||
field: &PatternField,
|
||||
metadata: &ClientMetadata,
|
||||
path_expr: &str,
|
||||
) -> String {
|
||||
if metadata.is_pattern_type(&field.rust_type) {
|
||||
syntax.constructor(&field.rust_type, path_expr)
|
||||
} else if let Some(accessor) = metadata.find_index_set_pattern(&field.indexes) {
|
||||
syntax.constructor(&accessor.name, path_expr)
|
||||
} else if field.is_branch() {
|
||||
syntax.constructor(&field.rust_type, path_expr)
|
||||
} else {
|
||||
// Templated child patterns receive acc and disc as separate arguments
|
||||
if let Some(child_pattern) = metadata.find_pattern(&field.rust_type)
|
||||
&& child_pattern.is_templated()
|
||||
{
|
||||
let disc_template = pattern.get_field_part(&field.name).unwrap_or(&field.name);
|
||||
let disc_arg = syntax.disc_arg_expr(disc_template);
|
||||
let acc_arg = syntax.owned_expr("acc");
|
||||
return syntax.constructor(&field.rust_type, &format!("{acc_arg}, {disc_arg}"));
|
||||
}
|
||||
|
||||
// Compute path expression from pattern mode
|
||||
let path_expr = match pattern.get_field_part(&field.name) {
|
||||
Some(part) => match &pattern.mode {
|
||||
Some(PatternMode::Templated { .. }) => syntax.template_expr("acc", part),
|
||||
Some(PatternMode::Prefix { .. }) => syntax.prefix_expr(part, "acc"),
|
||||
_ => syntax.suffix_expr("acc", part),
|
||||
},
|
||||
None => syntax.path_expr("acc", &path_suffix(&field.name)),
|
||||
};
|
||||
|
||||
// Wrap in constructor — leaves use their index accessor, everything else uses the type name
|
||||
if let Some(accessor) = metadata.find_index_set_pattern(&field.indexes) {
|
||||
syntax.constructor(&accessor.name, &path_expr)
|
||||
} else if field.is_leaf() {
|
||||
panic!(
|
||||
"Field '{}' has no matching pattern or index accessor. All metrics must be indexed.",
|
||||
"Field '{}' has no matching index accessor. All series must be indexed.",
|
||||
field.name
|
||||
)
|
||||
} else {
|
||||
syntax.constructor(&field.rust_type, &path_expr)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a parameterized field using the language syntax.
|
||||
/// Generate a parameterized field for a pattern factory.
|
||||
///
|
||||
/// This is used for pattern instances where fields use an accumulated
|
||||
/// metric name that's built up through the tree traversal.
|
||||
/// Used for pattern instances where fields build series names from an accumulated base.
|
||||
pub fn generate_parameterized_field<S: LanguageSyntax>(
|
||||
output: &mut String,
|
||||
syntax: &S,
|
||||
@@ -73,67 +76,69 @@ pub fn generate_parameterized_field<S: LanguageSyntax>(
|
||||
indent: &str,
|
||||
) {
|
||||
let field_name = syntax.field_name(&field.name);
|
||||
let type_ann = metadata.field_type_annotation(field, pattern.is_generic, None, syntax.generic_syntax());
|
||||
let path_expr = compute_path_expr(syntax, pattern, field, "acc");
|
||||
let value = compute_field_value(syntax, field, metadata, &path_expr);
|
||||
let type_ann =
|
||||
metadata.field_type_annotation(field, pattern.is_generic, None, syntax.generic_syntax());
|
||||
let value = compute_parameterized_value(syntax, field, pattern, metadata);
|
||||
|
||||
writeln!(output, "{}", syntax.field_init(indent, &field_name, &type_ann, &value)).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
"{}",
|
||||
syntax.field_init(indent, &field_name, &type_ann, &value)
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Generate a tree node field with a specific child node for pattern instance base detection.
|
||||
/// Generate a tree node field for a pattern-type child.
|
||||
///
|
||||
/// This is used when generating tree nodes where we need to detect the pattern instance
|
||||
/// base from descendant leaf names.
|
||||
/// Called for non-inline branch children that match a parameterizable pattern.
|
||||
/// For templated patterns, extracts the discriminator from the base result.
|
||||
pub fn generate_tree_node_field<S: LanguageSyntax>(
|
||||
output: &mut String,
|
||||
syntax: &S,
|
||||
field: &PatternField,
|
||||
metadata: &ClientMetadata,
|
||||
indent: &str,
|
||||
child_name: &str,
|
||||
pattern_base: Option<&str>,
|
||||
client_expr: &str,
|
||||
base_result: &PatternBaseResult,
|
||||
) {
|
||||
let field_name = syntax.field_name(&field.name);
|
||||
let type_ann = metadata.field_type_annotation(field, false, None, syntax.generic_syntax());
|
||||
let base_arg = syntax.string_literal(&base_result.base);
|
||||
|
||||
let value = if metadata.is_pattern_type(&field.rust_type) {
|
||||
// Use metric base only for parameterizable patterns
|
||||
let use_base = metadata
|
||||
.find_pattern(&field.rust_type)
|
||||
.is_some_and(|p| p.is_parameterizable())
|
||||
&& pattern_base.is_some();
|
||||
|
||||
let path_arg = if use_base {
|
||||
syntax.string_literal(pattern_base.unwrap())
|
||||
} else {
|
||||
syntax.path_expr("base_path", &path_suffix(child_name))
|
||||
};
|
||||
syntax.constructor(&field.rust_type, &path_arg)
|
||||
} else if let Some(accessor) = metadata.find_index_set_pattern(&field.indexes) {
|
||||
// Leaf field - use metric name if provided, else tree path
|
||||
let path_arg = pattern_base
|
||||
.map(|name| syntax.string_literal(name))
|
||||
.unwrap_or_else(|| syntax.path_expr("base_path", &path_suffix(child_name)));
|
||||
syntax.constructor(&accessor.name, &path_arg)
|
||||
} else if field.is_branch() {
|
||||
// Non-pattern branch - instantiate the nested struct
|
||||
let path_expr = syntax.path_expr("base_path", &path_suffix(child_name));
|
||||
syntax.constructor(&field.rust_type, &path_expr)
|
||||
let value = if let Some(pattern) = metadata.find_pattern(&field.rust_type)
|
||||
&& pattern.is_templated()
|
||||
{
|
||||
let disc = pattern
|
||||
.extract_disc_from_instance(&base_result.field_parts)
|
||||
.unwrap_or_default();
|
||||
format!(
|
||||
"{}({}, {}, {})",
|
||||
syntax.constructor_name(&field.rust_type),
|
||||
client_expr,
|
||||
base_arg,
|
||||
syntax.string_literal(&disc)
|
||||
)
|
||||
} else {
|
||||
// All metrics must be indexed
|
||||
panic!(
|
||||
"Field '{}' is a leaf with no index accessor. All metrics must be indexed.",
|
||||
field.name
|
||||
format!(
|
||||
"{}({}, {})",
|
||||
syntax.constructor_name(&field.rust_type),
|
||||
client_expr,
|
||||
base_arg
|
||||
)
|
||||
};
|
||||
|
||||
writeln!(output, "{}", syntax.field_init(indent, &field_name, &type_ann, &value)).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
"{}",
|
||||
syntax.field_init(indent, &field_name, &type_ann, &value)
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Generate a leaf field using the actual metric name from the TreeNode::Leaf.
|
||||
/// Generate a leaf field using the actual series name from the TreeNode::Leaf.
|
||||
///
|
||||
/// This is the shared implementation for all language backends. It uses
|
||||
/// `leaf.name()` directly to get the correct metric name, avoiding any
|
||||
/// `leaf.name()` directly to get the correct series name, avoiding any
|
||||
/// path concatenation that could produce incorrect names.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -141,7 +146,7 @@ pub fn generate_tree_node_field<S: LanguageSyntax>(
|
||||
/// * `syntax` - The language syntax implementation
|
||||
/// * `client_expr` - The client expression (e.g., "client.clone()", "this", "client")
|
||||
/// * `tree_field_name` - The field name from the tree structure
|
||||
/// * `leaf` - The Leaf node containing the actual metric name and indexes
|
||||
/// * `leaf` - The Leaf node containing the actual series name and indexes
|
||||
/// * `metadata` - Client metadata for looking up index patterns
|
||||
/// * `indent` - Indentation string
|
||||
pub fn generate_leaf_field<S: LanguageSyntax>(
|
||||
@@ -149,7 +154,7 @@ pub fn generate_leaf_field<S: LanguageSyntax>(
|
||||
syntax: &S,
|
||||
client_expr: &str,
|
||||
tree_field_name: &str,
|
||||
leaf: &MetricLeafWithSchema,
|
||||
leaf: &SeriesLeafWithSchema,
|
||||
metadata: &ClientMetadata,
|
||||
indent: &str,
|
||||
) {
|
||||
@@ -158,18 +163,18 @@ pub fn generate_leaf_field<S: LanguageSyntax>(
|
||||
.find_index_set_pattern(leaf.indexes())
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"Metric '{}' has no matching index pattern. All metrics must be indexed.",
|
||||
"Series '{}' has no matching index pattern. All series must be indexed.",
|
||||
leaf.name()
|
||||
)
|
||||
});
|
||||
|
||||
let type_ann = metadata.field_type_annotation_from_leaf(leaf, syntax.generic_syntax());
|
||||
let metric_name = syntax.string_literal(leaf.name());
|
||||
let series_name = syntax.string_literal(leaf.name());
|
||||
let value = format!(
|
||||
"{}({}, {})",
|
||||
syntax.constructor_name(&accessor.name),
|
||||
client_expr,
|
||||
metric_name
|
||||
series_name
|
||||
);
|
||||
|
||||
writeln!(
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
//! Shared tree generation helpers.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use brk_types::TreeNode;
|
||||
|
||||
use crate::{ClientMetadata, PatternBaseResult, PatternField, child_type_name, get_fields_with_child_info};
|
||||
use crate::{
|
||||
ClientMetadata, PatternBaseResult, PatternField, child_type_name, get_fields_with_child_info,
|
||||
};
|
||||
|
||||
/// Build a child path by appending a child name to a parent path.
|
||||
/// Uses "/" as separator. If parent is empty, returns just the child name.
|
||||
@@ -23,10 +25,8 @@ pub struct ChildContext<'a> {
|
||||
pub name: &'a str,
|
||||
/// The child node.
|
||||
pub node: &'a TreeNode,
|
||||
/// The field info for this child.
|
||||
/// The field info for this child (with type_param set for generic patterns).
|
||||
pub field: PatternField,
|
||||
/// Child fields if this is a branch (for pattern lookup).
|
||||
pub child_fields: Option<Vec<PatternField>>,
|
||||
/// Pattern analysis result.
|
||||
pub base_result: PatternBaseResult,
|
||||
/// Whether this is a leaf node.
|
||||
@@ -53,9 +53,9 @@ pub fn prepare_tree_node<'a>(
|
||||
node: &'a TreeNode,
|
||||
name: &str,
|
||||
path: &str,
|
||||
pattern_lookup: &HashMap<Vec<PatternField>, String>,
|
||||
pattern_lookup: &BTreeMap<Vec<PatternField>, String>,
|
||||
metadata: &ClientMetadata,
|
||||
generated: &mut HashSet<String>,
|
||||
generated: &mut BTreeSet<String>,
|
||||
) -> Option<TreeNodeContext<'a>> {
|
||||
let TreeNode::Branch(branch_children) = node else {
|
||||
return None;
|
||||
@@ -100,9 +100,14 @@ pub fn prepare_tree_node<'a>(
|
||||
let children: Vec<ChildContext<'a>> = branch_children
|
||||
.iter()
|
||||
.zip(fields_with_child_info)
|
||||
.map(|((child_name, child_node), (field, child_fields))| {
|
||||
.map(|((child_name, child_node), (mut field, child_fields))| {
|
||||
let is_leaf = matches!(child_node, TreeNode::Leaf(_));
|
||||
|
||||
// Set type_param for generic patterns so field_type_annotation works directly
|
||||
if let Some(cf) = &child_fields {
|
||||
field.type_param = metadata.get_type_param(cf).cloned();
|
||||
}
|
||||
|
||||
// Build child path and look up its pre-computed base result
|
||||
let child_path = build_child_path(path, child_name);
|
||||
let base_result = metadata
|
||||
@@ -110,27 +115,26 @@ pub fn prepare_tree_node<'a>(
|
||||
.cloned()
|
||||
.unwrap_or_else(PatternBaseResult::force_inline);
|
||||
|
||||
// For type annotations: use pattern type if ANY pattern matches
|
||||
let matches_any_pattern = child_fields
|
||||
// Single lookup for the child's matching pattern (avoids repeated scans)
|
||||
let matching_pattern = child_fields
|
||||
.as_ref()
|
||||
.is_some_and(|cf| metadata.matches_pattern(cf));
|
||||
.and_then(|cf| metadata.find_pattern_by_fields(cf));
|
||||
|
||||
// Check if the pattern mode AND field parts match the instance
|
||||
// Uses is_none_or so that "no pattern" doesn't trigger inlining
|
||||
let pattern_compatible = child_fields
|
||||
.as_ref()
|
||||
.and_then(|cf| metadata.find_pattern_by_fields(cf))
|
||||
.is_none_or(|p| {
|
||||
p.is_suffix_mode() == base_result.is_suffix_mode
|
||||
&& p.field_parts_match(&base_result.field_parts)
|
||||
});
|
||||
let matches_any_pattern = matching_pattern.is_some();
|
||||
let pattern_compatible = matching_pattern.is_none_or(|p| {
|
||||
p.is_suffix_mode() == base_result.is_suffix_mode
|
||||
&& p.field_parts_match(&base_result.field_parts)
|
||||
});
|
||||
let is_parameterizable =
|
||||
matching_pattern.is_none_or(|p| metadata.is_parameterizable(&p.name));
|
||||
|
||||
// should_inline determines if we generate an inline struct type
|
||||
// We inline if: it's a branch AND (doesn't match any pattern OR pattern incompatible OR has outlier)
|
||||
let should_inline =
|
||||
!is_leaf && (!matches_any_pattern || !pattern_compatible || base_result.has_outlier);
|
||||
let should_inline = !is_leaf
|
||||
&& (!matches_any_pattern
|
||||
|| !pattern_compatible
|
||||
|| !is_parameterizable
|
||||
|| base_result.has_outlier);
|
||||
|
||||
// Inline type name (only used when should_inline is true)
|
||||
let inline_type_name = if should_inline {
|
||||
child_type_name(name, child_name)
|
||||
} else {
|
||||
@@ -141,7 +145,6 @@ pub fn prepare_tree_node<'a>(
|
||||
name: child_name,
|
||||
node: child_node,
|
||||
field,
|
||||
child_fields,
|
||||
base_result,
|
||||
is_leaf,
|
||||
should_inline,
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
use crate::{Endpoint, Parameter, generators::{normalize_return_type, write_description}, to_camel_case};
|
||||
use crate::{
|
||||
Endpoint, Parameter,
|
||||
generators::{javascript::types::jsdoc_normalize, normalize_return_type, write_description},
|
||||
to_camel_case,
|
||||
};
|
||||
|
||||
/// Generate API methods for the BrkClient class.
|
||||
pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
@@ -10,98 +14,250 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
if !endpoint.should_generate() {
|
||||
continue;
|
||||
}
|
||||
match endpoint.method.as_str() {
|
||||
"GET" => generate_get_method(output, endpoint),
|
||||
"POST" => generate_post_method(output, endpoint),
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let method_name = endpoint_to_method_name(endpoint);
|
||||
let base_return_type =
|
||||
normalize_return_type(endpoint.response_type.as_deref().unwrap_or("*"));
|
||||
let return_type = if endpoint.supports_csv {
|
||||
format!("{} | string", base_return_type)
|
||||
fn generate_get_method(output: &mut String, endpoint: &Endpoint) {
|
||||
let method_name = endpoint_to_method_name(endpoint);
|
||||
let return_type = build_return_type(endpoint);
|
||||
|
||||
write_method_doc(output, endpoint);
|
||||
for param in &endpoint.path_params {
|
||||
let desc = format_param_desc(param.description.as_deref());
|
||||
let ty = jsdoc_normalize(¶m.param_type);
|
||||
writeln!(output, " * @param {{{}}} {}{}", ty, param.name, desc).unwrap();
|
||||
}
|
||||
for param in &endpoint.query_params {
|
||||
let optional = if param.required { "" } else { "=" };
|
||||
let desc = format_param_desc(param.description.as_deref());
|
||||
let ty = jsdoc_normalize(¶m.param_type);
|
||||
let ident = sanitize_ident(¶m.name);
|
||||
let name_decl = if param.required {
|
||||
ident
|
||||
} else {
|
||||
base_return_type
|
||||
format!("[{}]", ident)
|
||||
};
|
||||
writeln!(
|
||||
output,
|
||||
" * @param {{{}{}}} {}{}",
|
||||
ty, optional, name_decl, desc
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
writeln!(
|
||||
output,
|
||||
" * @param {{{{ signal?: AbortSignal, onValue?: (value: {}) => void }}}} [options]",
|
||||
return_type
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " * @returns {{Promise<{}>}}", return_type).unwrap();
|
||||
writeln!(output, " */").unwrap();
|
||||
|
||||
writeln!(output, " /**").unwrap();
|
||||
if let Some(summary) = &endpoint.summary {
|
||||
writeln!(output, " * {}", summary).unwrap();
|
||||
}
|
||||
if let Some(desc) = &endpoint.description
|
||||
&& endpoint.summary.as_ref() != Some(desc)
|
||||
{
|
||||
writeln!(output, " *").unwrap();
|
||||
write_description(output, desc, " * ", " *");
|
||||
}
|
||||
let params = build_method_params(endpoint);
|
||||
let params_with_opts = if params.is_empty() {
|
||||
"{ signal, onValue } = {}".to_string()
|
||||
} else {
|
||||
format!("{}, {{ signal, onValue }} = {{}}", params)
|
||||
};
|
||||
writeln!(output, " async {}({}) {{", method_name, params_with_opts).unwrap();
|
||||
|
||||
// Add endpoint path
|
||||
let path = build_path_template(&endpoint.path, &endpoint.path_params);
|
||||
|
||||
let fetch_call: String = if endpoint.returns_binary() {
|
||||
"this.getBytes(path, { signal, onValue })".to_string()
|
||||
} else if endpoint.returns_json() {
|
||||
"this.getJson(path, { signal, onValue })".to_string()
|
||||
} else if endpoint.response_kind.text_is_numeric() {
|
||||
"Number(await this.getText(path, { signal, onValue: onValue ? (v) => onValue(Number(v)) : undefined }))".to_string()
|
||||
} else {
|
||||
"this.getText(path, { signal, onValue })".to_string()
|
||||
};
|
||||
|
||||
write_path_assignment(output, endpoint, &path);
|
||||
|
||||
if endpoint.supports_csv {
|
||||
writeln!(
|
||||
output,
|
||||
" if (format === 'csv') return this.getText(path, {{ signal, onValue }});"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
writeln!(output, " return {};", fetch_call).unwrap();
|
||||
writeln!(output, " }}\n").unwrap();
|
||||
}
|
||||
|
||||
fn generate_post_method(output: &mut String, endpoint: &Endpoint) {
|
||||
let method_name = endpoint_to_method_name(endpoint);
|
||||
let return_type = build_return_type(endpoint);
|
||||
|
||||
write_method_doc(output, endpoint);
|
||||
for param in &endpoint.path_params {
|
||||
let desc = format_param_desc(param.description.as_deref());
|
||||
let ty = jsdoc_normalize(¶m.param_type);
|
||||
writeln!(output, " * @param {{{}}} {}{}", ty, param.name, desc).unwrap();
|
||||
}
|
||||
for param in &endpoint.query_params {
|
||||
let optional = if param.required { "" } else { "=" };
|
||||
let desc = format_param_desc(param.description.as_deref());
|
||||
let ty = jsdoc_normalize(¶m.param_type);
|
||||
writeln!(
|
||||
output,
|
||||
" * @param {{{}{}}} [{}]{}",
|
||||
ty, optional, param.name, desc
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
if let Some(body) = &endpoint.request_body {
|
||||
let optional = if body.required { "" } else { "=" };
|
||||
let ty = jsdoc_normalize(&body.body_type);
|
||||
writeln!(
|
||||
output,
|
||||
" * @param {{{}{}}} body - Request body",
|
||||
ty, optional
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
writeln!(
|
||||
output,
|
||||
" * @param {{{{ signal?: AbortSignal }}}} [options]"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " * @returns {{Promise<{}>}}", return_type).unwrap();
|
||||
writeln!(output, " */").unwrap();
|
||||
|
||||
let mut params = build_method_params(endpoint);
|
||||
if endpoint.request_body.is_some() {
|
||||
if !params.is_empty() {
|
||||
params.push_str(", ");
|
||||
}
|
||||
params.push_str("body");
|
||||
}
|
||||
let params_with_opts = if params.is_empty() {
|
||||
"{ signal } = {}".to_string()
|
||||
} else {
|
||||
format!("{}, {{ signal }} = {{}}", params)
|
||||
};
|
||||
writeln!(output, " async {}({}) {{", method_name, params_with_opts).unwrap();
|
||||
|
||||
let path = build_path_template(&endpoint.path, &endpoint.path_params);
|
||||
let body_arg = if endpoint.request_body.is_some() {
|
||||
"body"
|
||||
} else {
|
||||
"''"
|
||||
};
|
||||
|
||||
let fetch_call: String = if endpoint.returns_binary() {
|
||||
format!("this.postBytes(path, {}, {{ signal }})", body_arg)
|
||||
} else if endpoint.returns_json() {
|
||||
format!("this.postJson(path, {}, {{ signal }})", body_arg)
|
||||
} else if endpoint.response_kind.text_is_numeric() {
|
||||
format!(
|
||||
"Number(await this.postText(path, {}, {{ signal }}))",
|
||||
body_arg
|
||||
)
|
||||
} else {
|
||||
format!("this.postText(path, {}, {{ signal }})", body_arg)
|
||||
};
|
||||
|
||||
write_path_assignment(output, endpoint, &path);
|
||||
|
||||
writeln!(output, " return {};", fetch_call).unwrap();
|
||||
writeln!(output, " }}\n").unwrap();
|
||||
}
|
||||
|
||||
fn build_return_type(endpoint: &Endpoint) -> String {
|
||||
let base = if endpoint.returns_binary() {
|
||||
"Uint8Array".to_string()
|
||||
} else {
|
||||
jsdoc_normalize(&normalize_return_type(
|
||||
endpoint.schema_name().unwrap_or("*"),
|
||||
))
|
||||
};
|
||||
if endpoint.supports_csv {
|
||||
format!("{} | string", base)
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
|
||||
fn write_method_doc(output: &mut String, endpoint: &Endpoint) {
|
||||
writeln!(output, " /**").unwrap();
|
||||
if let Some(summary) = &endpoint.summary {
|
||||
writeln!(output, " * {}", summary).unwrap();
|
||||
}
|
||||
if let Some(desc) = &endpoint.description
|
||||
&& endpoint.summary.as_ref() != Some(desc)
|
||||
{
|
||||
writeln!(output, " *").unwrap();
|
||||
writeln!(output, " * Endpoint: `{} {}`", endpoint.method.to_uppercase(), endpoint.path).unwrap();
|
||||
write_description(output, desc, " * ", " *");
|
||||
}
|
||||
writeln!(output, " *").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" * Endpoint: `{} {}`",
|
||||
endpoint.method.to_uppercase(),
|
||||
endpoint.path
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if !endpoint.path_params.is_empty() || !endpoint.query_params.is_empty() {
|
||||
writeln!(output, " *").unwrap();
|
||||
}
|
||||
let has_body_param = endpoint.method == "POST" && endpoint.request_body.is_some();
|
||||
if !endpoint.path_params.is_empty() || !endpoint.query_params.is_empty() || has_body_param {
|
||||
writeln!(output, " *").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
for param in &endpoint.path_params {
|
||||
let desc = format_param_desc(param.description.as_deref());
|
||||
writeln!(
|
||||
output,
|
||||
" * @param {{{}}} {}{}",
|
||||
param.param_type, param.name, desc
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
fn write_path_assignment(output: &mut String, endpoint: &Endpoint, path: &str) {
|
||||
if endpoint.query_params.is_empty() {
|
||||
writeln!(output, " const path = `{}`;", path).unwrap();
|
||||
} else {
|
||||
writeln!(output, " const params = new URLSearchParams();").unwrap();
|
||||
for param in &endpoint.query_params {
|
||||
let optional = if param.required { "" } else { "=" };
|
||||
let desc = format_param_desc(param.description.as_deref());
|
||||
writeln!(
|
||||
output,
|
||||
" * @param {{{}{}}} [{}]{}",
|
||||
param.param_type, optional, param.name, desc
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(output, " * @returns {{Promise<{}>}}", return_type).unwrap();
|
||||
writeln!(output, " */").unwrap();
|
||||
|
||||
let params = build_method_params(endpoint);
|
||||
writeln!(output, " async {}({}) {{", method_name, params).unwrap();
|
||||
|
||||
let path = build_path_template(&endpoint.path, &endpoint.path_params);
|
||||
|
||||
if endpoint.query_params.is_empty() {
|
||||
writeln!(output, " return this.getJson(`{}`);", path).unwrap();
|
||||
} else {
|
||||
writeln!(output, " const params = new URLSearchParams();").unwrap();
|
||||
for param in &endpoint.query_params {
|
||||
let ident = sanitize_ident(¶m.name);
|
||||
let is_array = param.param_type.ends_with("[]");
|
||||
if is_array {
|
||||
if param.required {
|
||||
writeln!(
|
||||
output,
|
||||
" params.set('{}', String({}));",
|
||||
param.name, param.name
|
||||
" for (const _v of {}) params.append('{}', String(_v));",
|
||||
ident, param.name
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
output,
|
||||
" if ({} !== undefined) params.set('{}', String({}));",
|
||||
param.name, param.name, param.name
|
||||
" if ({}) for (const _v of {}) params.append('{}', String(_v));",
|
||||
ident, ident, param.name
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
writeln!(output, " const query = params.toString();").unwrap();
|
||||
writeln!(output, " const path = `{}${{query ? '?' + query : ''}}`;", path).unwrap();
|
||||
|
||||
if endpoint.supports_csv {
|
||||
writeln!(output, " if (format === 'csv') {{").unwrap();
|
||||
writeln!(output, " return this.getText(path);").unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, " return this.getJson(path);").unwrap();
|
||||
} else if param.required {
|
||||
writeln!(
|
||||
output,
|
||||
" params.set('{}', String({}));",
|
||||
param.name, ident
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(output, " return this.getJson(path);").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" if ({} !== undefined) params.set('{}', String({}));",
|
||||
ident, param.name, ident
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(output, " }}\n").unwrap();
|
||||
writeln!(output, " const query = params.toString();").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" const path = `{}${{query ? '?' + query : ''}}`;",
|
||||
path
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,14 +268,19 @@ fn endpoint_to_method_name(endpoint: &Endpoint) -> String {
|
||||
fn build_method_params(endpoint: &Endpoint) -> String {
|
||||
let mut params = Vec::new();
|
||||
for param in &endpoint.path_params {
|
||||
params.push(param.name.clone());
|
||||
params.push(sanitize_ident(¶m.name));
|
||||
}
|
||||
for param in &endpoint.query_params {
|
||||
params.push(param.name.clone());
|
||||
params.push(sanitize_ident(¶m.name));
|
||||
}
|
||||
params.join(", ")
|
||||
}
|
||||
|
||||
/// Strip characters invalid in JS identifiers (e.g. `[]` from `txId[]`).
|
||||
fn sanitize_ident(name: &str) -> String {
|
||||
name.replace(['[', ']'], "")
|
||||
}
|
||||
|
||||
fn build_path_template(path: &str, path_params: &[Parameter]) -> String {
|
||||
let mut result = path.to_string();
|
||||
for param in path_params {
|
||||
|
||||
@@ -16,20 +16,38 @@ pub fn generate_base_client(output: &mut String) {
|
||||
* @typedef {{Object}} BrkClientOptions
|
||||
* @property {{string}} baseUrl - Base URL for the API
|
||||
* @property {{number}} [timeout] - Request timeout in milliseconds
|
||||
* @property {{string|boolean}} [cache] - Enable browser cache with default name (true), custom name (string), or disable (false). No effect in Node.js. Default: true
|
||||
* @property {{string|boolean}} [browserCache] - Enable browser Cache API with default name (true), custom name (string), or disable (false). No effect in Node.js. Default: true
|
||||
* @property {{number|boolean}} [memCache] - In-memory parsed-response cache size (LRU). true/undefined → 1000, false/0 → disabled. Lets 304 responses skip the JSON parse entirely. Default: 1000
|
||||
*/
|
||||
|
||||
const _isBrowser = typeof window !== 'undefined' && 'caches' in window;
|
||||
const _runIdle = (/** @type {{VoidFunction}} */ fn) => (globalThis.requestIdleCallback ?? setTimeout)(fn);
|
||||
const _defaultCacheName = '__BRK_CLIENT__';
|
||||
const _defaultBrowserCacheName = '__BRK_CLIENT__';
|
||||
const _DEFAULT_MEM_CACHE_SIZE = 1000;
|
||||
|
||||
/** @template T @typedef {{{{ etag: string | null, value: T }}}} _MemEntry */
|
||||
/** @param {{*}} v */
|
||||
const _addCamelGetters = (v) => {{
|
||||
if (Array.isArray(v)) {{ v.forEach(_addCamelGetters); return v; }}
|
||||
if (v && typeof v === 'object' && v.constructor === Object) {{
|
||||
for (const k in v) {{
|
||||
if (k.includes('_')) {{
|
||||
const c = k.replace(/_([a-z])/g, (_, l) => l.toUpperCase());
|
||||
if (!(c in v)) Object.defineProperty(v, c, {{ get() {{ return this[k]; }} }});
|
||||
}}
|
||||
_addCamelGetters(v[k]);
|
||||
}}
|
||||
}}
|
||||
return v;
|
||||
}};
|
||||
|
||||
/**
|
||||
* @param {{string|boolean|undefined}} cache
|
||||
* @param {{string|boolean|undefined}} option
|
||||
* @returns {{Promise<Cache | null>}}
|
||||
*/
|
||||
const _openCache = (cache) => {{
|
||||
if (!_isBrowser || cache === false) return Promise.resolve(null);
|
||||
const name = typeof cache === 'string' ? cache : _defaultCacheName;
|
||||
const _openBrowserCache = (option) => {{
|
||||
if (!_isBrowser || option === false) return Promise.resolve(null);
|
||||
const name = typeof option === 'string' ? option : _defaultBrowserCacheName;
|
||||
return caches.open(name).catch(() => null);
|
||||
}};
|
||||
|
||||
@@ -48,84 +66,262 @@ class BrkError extends Error {{
|
||||
}}
|
||||
}}
|
||||
|
||||
// Date conversion constants and helpers
|
||||
const _GENESIS = new Date(2009, 0, 3); // day1 0, week1 0
|
||||
const _DAY_ONE = new Date(2009, 0, 9); // day1 1 (6 day gap after genesis)
|
||||
const _MS_PER_DAY = 86400000;
|
||||
const _MS_PER_WEEK = 7 * _MS_PER_DAY;
|
||||
const _EPOCH_MS = 1230768000000;
|
||||
const _DATE_INDEXES = new Set([
|
||||
'minute10', 'minute30',
|
||||
'hour1', 'hour4', 'hour12',
|
||||
'day1', 'day3', 'week1',
|
||||
'month1', 'month3', 'month6',
|
||||
'year1', 'year10',
|
||||
]);
|
||||
|
||||
/** @param {{number}} months @returns {{globalThis.Date}} */
|
||||
const _addMonths = (months) => new Date(2009, months, 1);
|
||||
|
||||
/**
|
||||
* Convert an index value to a Date for date-based indexes.
|
||||
* @param {{Index}} index - The index type
|
||||
* @param {{number}} i - The index value
|
||||
* @returns {{globalThis.Date}}
|
||||
*/
|
||||
function indexToDate(index, i) {{
|
||||
switch (index) {{
|
||||
case 'minute10': return new Date(_EPOCH_MS + i * 600000);
|
||||
case 'minute30': return new Date(_EPOCH_MS + i * 1800000);
|
||||
case 'hour1': return new Date(_EPOCH_MS + i * 3600000);
|
||||
case 'hour4': return new Date(_EPOCH_MS + i * 14400000);
|
||||
case 'hour12': return new Date(_EPOCH_MS + i * 43200000);
|
||||
case 'day1': return i === 0 ? _GENESIS : new Date(_DAY_ONE.getTime() + (i - 1) * _MS_PER_DAY);
|
||||
case 'day3': return new Date(_EPOCH_MS - 86400000 + i * 259200000);
|
||||
case 'week1': return new Date(_GENESIS.getTime() + i * _MS_PER_WEEK);
|
||||
case 'month1': return _addMonths(i);
|
||||
case 'month3': return _addMonths(i * 3);
|
||||
case 'month6': return _addMonths(i * 6);
|
||||
case 'year1': return new Date(2009 + i, 0, 1);
|
||||
case 'year10': return new Date(2009 + i * 10, 0, 1);
|
||||
default: throw new Error(`${{index}} is not a date-based index`);
|
||||
}}
|
||||
}}
|
||||
|
||||
/**
|
||||
* Convert a Date to an index value for date-based indexes.
|
||||
* Returns the floor index (latest index whose date is <= the given date).
|
||||
* @param {{Index}} index - The index type
|
||||
* @param {{globalThis.Date}} d - The date to convert
|
||||
* @returns {{number}}
|
||||
*/
|
||||
function dateToIndex(index, d) {{
|
||||
const ms = d.getTime();
|
||||
switch (index) {{
|
||||
case 'minute10': return Math.floor((ms - _EPOCH_MS) / 600000);
|
||||
case 'minute30': return Math.floor((ms - _EPOCH_MS) / 1800000);
|
||||
case 'hour1': return Math.floor((ms - _EPOCH_MS) / 3600000);
|
||||
case 'hour4': return Math.floor((ms - _EPOCH_MS) / 14400000);
|
||||
case 'hour12': return Math.floor((ms - _EPOCH_MS) / 43200000);
|
||||
case 'day1': {{
|
||||
if (ms < _DAY_ONE.getTime()) return 0;
|
||||
return 1 + Math.floor((ms - _DAY_ONE.getTime()) / _MS_PER_DAY);
|
||||
}}
|
||||
case 'day3': return Math.floor((ms - _EPOCH_MS + 86400000) / 259200000);
|
||||
case 'week1': return Math.floor((ms - _GENESIS.getTime()) / _MS_PER_WEEK);
|
||||
case 'month1': return (d.getFullYear() - 2009) * 12 + d.getMonth();
|
||||
case 'month3': return (d.getFullYear() - 2009) * 4 + Math.floor(d.getMonth() / 3);
|
||||
case 'month6': return (d.getFullYear() - 2009) * 2 + Math.floor(d.getMonth() / 6);
|
||||
case 'year1': return d.getFullYear() - 2009;
|
||||
case 'year10': return Math.floor((d.getFullYear() - 2009) / 10);
|
||||
default: throw new Error(`${{index}} is not a date-based index`);
|
||||
}}
|
||||
}}
|
||||
|
||||
/**
|
||||
* Wrap raw series data with helper methods.
|
||||
* @template T
|
||||
* @param {{SeriesData<T>}} raw - Raw JSON response
|
||||
* @returns {{DateSeriesData<T>}}
|
||||
*/
|
||||
function _wrapSeriesData(raw) {{
|
||||
const {{ index, start, end, data }} = raw;
|
||||
const _dateBased = _DATE_INDEXES.has(index);
|
||||
return /** @type {{DateSeriesData<T>}} */ ({{
|
||||
...raw,
|
||||
isDateBased: _dateBased,
|
||||
indexes() {{
|
||||
/** @type {{number[]}} */
|
||||
const result = [];
|
||||
for (let i = start; i < end; i++) result.push(i);
|
||||
return result;
|
||||
}},
|
||||
keys() {{
|
||||
return this.indexes();
|
||||
}},
|
||||
entries() {{
|
||||
/** @type {{Array<[number, T]>}} */
|
||||
const result = [];
|
||||
for (let i = 0; i < data.length; i++) result.push([start + i, data[i]]);
|
||||
return result;
|
||||
}},
|
||||
toMap() {{
|
||||
/** @type {{Map<number, T>}} */
|
||||
const map = new Map();
|
||||
for (let i = 0; i < data.length; i++) map.set(start + i, data[i]);
|
||||
return map;
|
||||
}},
|
||||
*[Symbol.iterator]() {{
|
||||
for (let i = 0; i < data.length; i++) yield /** @type {{[number, T]}} */ ([start + i, data[i]]);
|
||||
}},
|
||||
// DateSeriesData methods (only meaningful for date-based indexes)
|
||||
dates() {{
|
||||
/** @type {{globalThis.Date[]}} */
|
||||
const result = [];
|
||||
for (let i = start; i < end; i++) result.push(indexToDate(index, i));
|
||||
return result;
|
||||
}},
|
||||
dateEntries() {{
|
||||
/** @type {{Array<[globalThis.Date, T]>}} */
|
||||
const result = [];
|
||||
for (let i = 0; i < data.length; i++) result.push([indexToDate(index, start + i), data[i]]);
|
||||
return result;
|
||||
}},
|
||||
toDateMap() {{
|
||||
/** @type {{Map<globalThis.Date, T>}} */
|
||||
const map = new Map();
|
||||
for (let i = 0; i < data.length; i++) map.set(indexToDate(index, start + i), data[i]);
|
||||
return map;
|
||||
}},
|
||||
}});
|
||||
}}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {{Object}} MetricData
|
||||
* @property {{number}} total - Total number of data points
|
||||
* @typedef {{Object}} SeriesDataBase
|
||||
* @property {{number}} version - Version of the series data
|
||||
* @property {{Index}} index - The index type used for this query
|
||||
* @property {{string}} type - Value type (e.g. "f32", "u64", "Sats")
|
||||
* @property {{number}} start - Start index (inclusive)
|
||||
* @property {{number}} end - End index (exclusive)
|
||||
* @property {{T[]}} data - The metric data
|
||||
*/
|
||||
/** @typedef {{MetricData<any>}} AnyMetricData */
|
||||
|
||||
/**
|
||||
* Thenable interface for await support.
|
||||
* @template T
|
||||
* @typedef {{(onfulfilled?: (value: MetricData<T>) => MetricData<T>, onrejected?: (reason: Error) => never) => Promise<MetricData<T>>}} Thenable
|
||||
* @property {{string}} stamp - ISO 8601 timestamp of when the response was generated
|
||||
* @property {{T[]}} data - The series data
|
||||
* @property {{boolean}} isDateBased - Whether this series uses a date-based index
|
||||
* @property {{() => number[]}} indexes - Get index numbers
|
||||
* @property {{() => number[]}} keys - Get keys as index numbers (alias for indexes)
|
||||
* @property {{() => Array<[number, T]>}} entries - Get [index, value] pairs
|
||||
* @property {{() => Map<number, T>}} toMap - Convert to Map<index, value>
|
||||
*/
|
||||
|
||||
/** @template T @typedef {{SeriesDataBase<T> & Iterable<[number, T]>}} SeriesData */
|
||||
|
||||
/**
|
||||
* Metric endpoint builder. Callable (returns itself) so both .by.dateindex and .by.dateindex() work.
|
||||
* @template T
|
||||
* @typedef {{Object}} MetricEndpointBuilder
|
||||
* @typedef {{Object}} DateSeriesDataExtras
|
||||
* @property {{() => globalThis.Date[]}} dates - Get dates for each data point
|
||||
* @property {{() => Array<[globalThis.Date, T]>}} dateEntries - Get [date, value] pairs
|
||||
* @property {{() => Map<globalThis.Date, T>}} toDateMap - Convert to Map<date, value>
|
||||
*/
|
||||
|
||||
/** @template T @typedef {{SeriesData<T> & DateSeriesDataExtras<T>}} DateSeriesData */
|
||||
/** @typedef {{SeriesData<any>}} AnySeriesData */
|
||||
|
||||
/** @template T @typedef {{(onfulfilled?: (value: SeriesData<T>) => any, onrejected?: (reason: Error) => never) => Promise<SeriesData<T>>}} Thenable */
|
||||
/** @template T @typedef {{(onfulfilled?: (value: DateSeriesData<T>) => any, onrejected?: (reason: Error) => never) => Promise<DateSeriesData<T>>}} DateThenable */
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {{Object}} SeriesEndpoint
|
||||
* @property {{(index: number) => SingleItemBuilder<T>}} get - Get single item at index
|
||||
* @property {{(start?: number, end?: number) => RangeBuilder<T>}} slice - Slice like Array.slice
|
||||
* @property {{(start?: number, end?: number) => RangeBuilder<T>}} slice - Slice by index
|
||||
* @property {{(n: number) => RangeBuilder<T>}} first - Get first n items
|
||||
* @property {{(n: number) => RangeBuilder<T>}} last - Get last n items
|
||||
* @property {{(n: number) => SkippedBuilder<T>}} skip - Skip first n items, chain with take()
|
||||
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch all data
|
||||
* @property {{(onValue?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch all data
|
||||
* @property {{() => Promise<string>}} fetchCsv - Fetch all data as CSV
|
||||
* @property {{() => Promise<number>}} len - Get total number of data points
|
||||
* @property {{() => Promise<Version>}} version - Get the current version of the series
|
||||
* @property {{Thenable<T>}} then - Thenable (await endpoint)
|
||||
* @property {{string}} path - The endpoint path
|
||||
*/
|
||||
/** @typedef {{MetricEndpointBuilder<any>}} AnyMetricEndpointBuilder */
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {{Object}} SingleItemBuilder
|
||||
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch the item
|
||||
* @typedef {{Object}} DateSeriesEndpoint
|
||||
* @property {{(index: number | globalThis.Date) => DateSingleItemBuilder<T>}} get - Get single item at index or Date
|
||||
* @property {{(start?: number | globalThis.Date, end?: number | globalThis.Date) => DateRangeBuilder<T>}} slice - Slice by index or Date
|
||||
* @property {{(n: number) => DateRangeBuilder<T>}} first - Get first n items
|
||||
* @property {{(n: number) => DateRangeBuilder<T>}} last - Get last n items
|
||||
* @property {{(n: number) => DateSkippedBuilder<T>}} skip - Skip first n items, chain with take()
|
||||
* @property {{(onValue?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch all data
|
||||
* @property {{() => Promise<string>}} fetchCsv - Fetch all data as CSV
|
||||
* @property {{() => Promise<number>}} len - Get total number of data points
|
||||
* @property {{() => Promise<Version>}} version - Get the current version of the series
|
||||
* @property {{DateThenable<T>}} then - Thenable (await endpoint)
|
||||
* @property {{string}} path - The endpoint path
|
||||
*/
|
||||
|
||||
/** @typedef {{SeriesEndpoint<any>}} AnySeriesEndpoint */
|
||||
|
||||
/** @template T @typedef {{Object}} SingleItemBuilder
|
||||
* @property {{(onValue?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch the item
|
||||
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
|
||||
* @property {{Thenable<T>}} then - Thenable
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {{Object}} SkippedBuilder
|
||||
/** @template T @typedef {{Object}} DateSingleItemBuilder
|
||||
* @property {{(onValue?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch the item
|
||||
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
|
||||
* @property {{DateThenable<T>}} then - Thenable
|
||||
*/
|
||||
|
||||
/** @template T @typedef {{Object}} SkippedBuilder
|
||||
* @property {{(n: number) => RangeBuilder<T>}} take - Take n items after skipped position
|
||||
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch from skipped position to end
|
||||
* @property {{(onValue?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch from skipped position to end
|
||||
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
|
||||
* @property {{Thenable<T>}} then - Thenable
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {{Object}} RangeBuilder
|
||||
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch the range
|
||||
/** @template T @typedef {{Object}} DateSkippedBuilder
|
||||
* @property {{(n: number) => DateRangeBuilder<T>}} take - Take n items after skipped position
|
||||
* @property {{(onValue?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch from skipped position to end
|
||||
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
|
||||
* @property {{DateThenable<T>}} then - Thenable
|
||||
*/
|
||||
|
||||
/** @template T @typedef {{Object}} RangeBuilder
|
||||
* @property {{(onValue?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch the range
|
||||
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
|
||||
* @property {{Thenable<T>}} then - Thenable
|
||||
*/
|
||||
|
||||
/** @template T @typedef {{Object}} DateRangeBuilder
|
||||
* @property {{(onValue?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch the range
|
||||
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
|
||||
* @property {{DateThenable<T>}} then - Thenable
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {{Object}} MetricPattern
|
||||
* @property {{string}} name - The metric name
|
||||
* @property {{Readonly<Partial<Record<Index, MetricEndpointBuilder<T>>>>}} by - Index endpoints as lazy getters. Access via .by.dateindex or .by['dateindex']
|
||||
* @typedef {{Object}} SeriesPattern
|
||||
* @property {{string}} name - The series name
|
||||
* @property {{Readonly<Partial<Record<Index, SeriesEndpoint<T>>>>}} by - Index endpoints as lazy getters
|
||||
* @property {{() => readonly Index[]}} indexes - Get the list of available indexes
|
||||
* @property {{(index: Index) => MetricEndpointBuilder<T>|undefined}} get - Get an endpoint for a specific index
|
||||
* @property {{(index: Index) => SeriesEndpoint<T>|undefined}} get - Get an endpoint for a specific index
|
||||
*/
|
||||
|
||||
/** @typedef {{MetricPattern<any>}} AnyMetricPattern */
|
||||
/** @typedef {{SeriesPattern<any>}} AnySeriesPattern */
|
||||
|
||||
/**
|
||||
* Create a metric endpoint builder with typestate pattern.
|
||||
* Create a series endpoint builder with typestate pattern.
|
||||
* @template T
|
||||
* @param {{BrkClientBase}} client
|
||||
* @param {{string}} name - The metric vec name
|
||||
* @param {{BrkClient}} client
|
||||
* @param {{string}} name - The series vec name
|
||||
* @param {{Index}} index - The index name
|
||||
* @returns {{MetricEndpointBuilder<T>}}
|
||||
* @returns {{DateSeriesEndpoint<T>}}
|
||||
*/
|
||||
function _endpoint(client, name, index) {{
|
||||
const p = `/api/metric/${{name}}/${{index}}`;
|
||||
const p = `/api/series/${{name}}/${{index}}`;
|
||||
|
||||
/**
|
||||
* @param {{number}} [start]
|
||||
@@ -145,44 +341,50 @@ function _endpoint(client, name, index) {{
|
||||
/**
|
||||
* @param {{number}} [start]
|
||||
* @param {{number}} [end]
|
||||
* @returns {{RangeBuilder<T>}}
|
||||
* @returns {{DateRangeBuilder<T>}}
|
||||
*/
|
||||
const rangeBuilder = (start, end) => ({{
|
||||
fetch(onUpdate) {{ return client.getJson(buildPath(start, end), onUpdate); }},
|
||||
fetch(onValue) {{ return client._fetchSeriesData(buildPath(start, end), onValue); }},
|
||||
fetchCsv() {{ return client.getText(buildPath(start, end, 'csv')); }},
|
||||
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
|
||||
}});
|
||||
|
||||
/**
|
||||
* @param {{number}} index
|
||||
* @returns {{SingleItemBuilder<T>}}
|
||||
* @param {{number}} idx
|
||||
* @returns {{DateSingleItemBuilder<T>}}
|
||||
*/
|
||||
const singleItemBuilder = (index) => ({{
|
||||
fetch(onUpdate) {{ return client.getJson(buildPath(index, index + 1), onUpdate); }},
|
||||
fetchCsv() {{ return client.getText(buildPath(index, index + 1, 'csv')); }},
|
||||
const singleItemBuilder = (idx) => ({{
|
||||
fetch(onValue) {{ return client._fetchSeriesData(buildPath(idx, idx + 1), onValue); }},
|
||||
fetchCsv() {{ return client.getText(buildPath(idx, idx + 1, 'csv')); }},
|
||||
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
|
||||
}});
|
||||
|
||||
/**
|
||||
* @param {{number}} start
|
||||
* @returns {{SkippedBuilder<T>}}
|
||||
* @returns {{DateSkippedBuilder<T>}}
|
||||
*/
|
||||
const skippedBuilder = (start) => ({{
|
||||
take(n) {{ return rangeBuilder(start, start + n); }},
|
||||
fetch(onUpdate) {{ return client.getJson(buildPath(start, undefined), onUpdate); }},
|
||||
fetch(onValue) {{ return client._fetchSeriesData(buildPath(start, undefined), onValue); }},
|
||||
fetchCsv() {{ return client.getText(buildPath(start, undefined, 'csv')); }},
|
||||
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
|
||||
}});
|
||||
|
||||
/** @type {{MetricEndpointBuilder<T>}} */
|
||||
/** @type {{DateSeriesEndpoint<T>}} */
|
||||
const endpoint = {{
|
||||
get(index) {{ return singleItemBuilder(index); }},
|
||||
slice(start, end) {{ return rangeBuilder(start, end); }},
|
||||
get(idx) {{ if (idx instanceof Date) idx = dateToIndex(index, idx); return singleItemBuilder(idx); }},
|
||||
slice(start, end) {{
|
||||
if (start instanceof Date) start = dateToIndex(index, start);
|
||||
if (end instanceof Date) end = dateToIndex(index, end);
|
||||
return rangeBuilder(start, end);
|
||||
}},
|
||||
first(n) {{ return rangeBuilder(undefined, n); }},
|
||||
last(n) {{ return n === 0 ? rangeBuilder(undefined, 0) : rangeBuilder(-n, undefined); }},
|
||||
skip(n) {{ return skippedBuilder(n); }},
|
||||
fetch(onUpdate) {{ return client.getJson(buildPath(), onUpdate); }},
|
||||
fetch(onValue) {{ return client._fetchSeriesData(buildPath(), onValue); }},
|
||||
fetchCsv() {{ return client.getText(buildPath(undefined, undefined, 'csv')); }},
|
||||
len() {{ return client.getSeriesLen(name, index); }},
|
||||
version() {{ return client.getSeriesVersion(name, index); }},
|
||||
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
|
||||
get path() {{ return p; }},
|
||||
}};
|
||||
@@ -199,80 +401,265 @@ class BrkClientBase {{
|
||||
*/
|
||||
constructor(options) {{
|
||||
const isString = typeof options === 'string';
|
||||
this.baseUrl = isString ? options : options.baseUrl;
|
||||
const rawUrl = isString ? options : options.baseUrl;
|
||||
this.baseUrl = rawUrl.endsWith('/') ? rawUrl.slice(0, -1) : rawUrl;
|
||||
this.timeout = isString ? 5000 : (options.timeout ?? 5000);
|
||||
/** @type {{Promise<Cache | null>}} */
|
||||
this._cachePromise = _openCache(isString ? undefined : options.cache);
|
||||
this._browserCachePromise = _openBrowserCache(isString ? undefined : options.browserCache);
|
||||
/** @type {{Cache | null}} */
|
||||
this._browserCache = null;
|
||||
this._browserCachePromise.then(c => this._browserCache = c);
|
||||
const memOpt = isString ? undefined : options.memCache;
|
||||
this._memCacheMax = memOpt === false || memOpt === 0
|
||||
? 0
|
||||
: (typeof memOpt === 'number' ? memOpt : _DEFAULT_MEM_CACHE_SIZE);
|
||||
/** @type {{Map<string, _MemEntry<unknown>>}} */
|
||||
this._memCache = new Map();
|
||||
}}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {{string}} key
|
||||
* @returns {{_MemEntry<T> | undefined}}
|
||||
*/
|
||||
_memGet(key) {{
|
||||
if (!this._memCacheMax) return undefined;
|
||||
const hit = this._memCache.get(key);
|
||||
if (!hit) return undefined;
|
||||
this._memCache.delete(key);
|
||||
this._memCache.set(key, hit);
|
||||
return /** @type {{_MemEntry<T>}} */ (hit);
|
||||
}}
|
||||
|
||||
/**
|
||||
* @param {{string}} key
|
||||
* @param {{string | null}} etag
|
||||
* @param {{unknown}} value
|
||||
*/
|
||||
_memSet(key, etag, value) {{
|
||||
if (!this._memCacheMax) return;
|
||||
if (this._memCache.has(key)) this._memCache.delete(key);
|
||||
else if (this._memCache.size >= this._memCacheMax) {{
|
||||
const oldest = this._memCache.keys().next().value;
|
||||
if (oldest !== undefined) this._memCache.delete(oldest);
|
||||
}}
|
||||
this._memCache.set(key, {{ etag, value }});
|
||||
}}
|
||||
|
||||
/**
|
||||
* @param {{string}} path
|
||||
* @param {{{{ signal?: AbortSignal }}}} [options]
|
||||
* @returns {{Promise<Response>}}
|
||||
*/
|
||||
async get(path) {{
|
||||
const base = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
|
||||
const url = `${{base}}${{path}}`;
|
||||
const res = await fetch(url, {{ signal: AbortSignal.timeout(this.timeout) }});
|
||||
async get(path, {{ signal }} = {{}}) {{
|
||||
const url = `${{this.baseUrl}}${{path}}`;
|
||||
const signals = [AbortSignal.timeout(this.timeout)];
|
||||
if (signal) signals.push(signal);
|
||||
const res = await fetch(url, {{ signal: AbortSignal.any(signals) }});
|
||||
if (!res.ok) throw new BrkError(`HTTP ${{res.status}}: ${{url}}`, res.status);
|
||||
return res;
|
||||
}}
|
||||
|
||||
/**
|
||||
* Make a GET request with stale-while-revalidate caching
|
||||
* Make a GET request with layered caching.
|
||||
*
|
||||
* Contract:
|
||||
* - The returned Promise resolves with the **freshest** value (post-revalidation).
|
||||
* - `onValue` fires once with the freshest value, or twice if a stale snapshot
|
||||
* could be shown first (stale-while-revalidate). On a 304 there is no second fire.
|
||||
*
|
||||
* Layers:
|
||||
* - L1 (memCache): in-memory parsed values keyed by URL+ETag. Lets 304s skip the parse entirely.
|
||||
* - L2 (browserCache): Cache API, survives reload and feeds onValue fast on cold start.
|
||||
*
|
||||
* @template T
|
||||
* @param {{string}} path
|
||||
* @param {{(value: T) => void}} [onUpdate] - Called when data is available
|
||||
* @param {{(res: Response) => Promise<T>}} parse - Response body reader
|
||||
* @param {{{{ onValue?: (value: T) => void, signal?: AbortSignal }}}} [options]
|
||||
* @returns {{Promise<T>}}
|
||||
*/
|
||||
async getJson(path, onUpdate) {{
|
||||
const base = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
|
||||
const url = `${{base}}${{path}}`;
|
||||
const cache = await this._cachePromise;
|
||||
const cachedRes = await cache?.match(url);
|
||||
const cachedJson = cachedRes ? await cachedRes.json() : null;
|
||||
async _getCached(path, parse, {{ onValue, signal }} = {{}}) {{
|
||||
const url = `${{this.baseUrl}}${{path}}`;
|
||||
/** @type {{_MemEntry<T> | undefined}} */
|
||||
const memHit = this._memGet(url);
|
||||
const browserCache = this._browserCache ?? await this._browserCachePromise;
|
||||
|
||||
if (cachedJson) onUpdate?.(cachedJson);
|
||||
if (globalThis.navigator?.onLine === false) {{
|
||||
if (cachedJson) return cachedJson;
|
||||
throw new BrkError('Offline and no cached data available');
|
||||
// L1 fast path: deliver from memCache, revalidate via network.
|
||||
// ETag match → zero parse, zero clone, zero cache write, no second onValue fire.
|
||||
if (memHit) {{
|
||||
if (onValue) onValue(memHit.value);
|
||||
try {{
|
||||
const res = await this.get(path, {{ signal }});
|
||||
const netEtag = res.headers.get('ETag');
|
||||
if (netEtag && netEtag === memHit.etag) return memHit.value;
|
||||
const cloned = browserCache ? res.clone() : null;
|
||||
const value = await parse(res);
|
||||
this._memSet(url, netEtag, value);
|
||||
if (onValue) onValue(value);
|
||||
if (cloned && browserCache) {{
|
||||
const cache = browserCache;
|
||||
_runIdle(() => cache.put(url, cloned));
|
||||
}}
|
||||
return value;
|
||||
}} catch {{
|
||||
return memHit.value;
|
||||
}}
|
||||
}}
|
||||
|
||||
try {{
|
||||
const res = await this.get(path);
|
||||
if (cachedRes?.headers.get('ETag') === res.headers.get('ETag')) return cachedJson;
|
||||
// L1 miss: race browserCache (stale snapshot) vs network (fresh).
|
||||
let networkSettled = false;
|
||||
const stalePromise = onValue && browserCache
|
||||
? browserCache.match(url).then(async (res) => {{
|
||||
if (!res || networkSettled) return null;
|
||||
const value = await parse(res);
|
||||
if (networkSettled) return value;
|
||||
this._memSet(url, res.headers.get('ETag'), value);
|
||||
onValue(value);
|
||||
return value;
|
||||
}}).catch(() => null)
|
||||
: null;
|
||||
|
||||
const cloned = res.clone();
|
||||
const json = await res.json();
|
||||
onUpdate?.(json);
|
||||
if (cache) _runIdle(() => cache.put(url, cloned));
|
||||
return json;
|
||||
try {{
|
||||
const res = await this.get(path, {{ signal }});
|
||||
networkSettled = true;
|
||||
const netEtag = res.headers.get('ETag');
|
||||
// Stale won and populated memCache with matching ETag → reuse, skip parse + second onValue.
|
||||
const populated = /** @type {{_MemEntry<T> | undefined}} */ (this._memGet(url));
|
||||
if (populated && netEtag && netEtag === populated.etag) return populated.value;
|
||||
const cloned = browserCache ? res.clone() : null;
|
||||
const value = await parse(res);
|
||||
this._memSet(url, netEtag, value);
|
||||
if (onValue) onValue(value);
|
||||
if (cloned && browserCache) {{
|
||||
const cache = browserCache;
|
||||
_runIdle(() => cache.put(url, cloned));
|
||||
}}
|
||||
return value;
|
||||
}} catch (e) {{
|
||||
if (cachedJson) return cachedJson;
|
||||
const stale = await stalePromise;
|
||||
if (stale != null) return stale;
|
||||
throw e;
|
||||
}}
|
||||
}}
|
||||
|
||||
/**
|
||||
* Make a GET request and return raw text (for CSV responses)
|
||||
* Make a GET request expecting a JSON response. Cached and supports `onValue`.
|
||||
* @template T
|
||||
* @param {{string}} path
|
||||
* @param {{{{ onValue?: (value: T) => void, signal?: AbortSignal }}}} [options]
|
||||
* @returns {{Promise<T>}}
|
||||
*/
|
||||
getJson(path, options) {{
|
||||
return this._getCached(path, async (res) => _addCamelGetters(await res.json()), options);
|
||||
}}
|
||||
|
||||
/**
|
||||
* Make a GET request expecting a text response (text/plain, text/csv, ...).
|
||||
* Cached and supports `onValue`, same as `getJson`.
|
||||
* @param {{string}} path
|
||||
* @param {{{{ onValue?: (value: string) => void, signal?: AbortSignal }}}} [options]
|
||||
* @returns {{Promise<string>}}
|
||||
*/
|
||||
async getText(path) {{
|
||||
const res = await this.get(path);
|
||||
getText(path, options) {{
|
||||
return this._getCached(path, (res) => res.text(), options);
|
||||
}}
|
||||
|
||||
/**
|
||||
* Make a GET request expecting binary data (application/octet-stream).
|
||||
* Cached and supports `onValue`, same as `getJson`.
|
||||
* @param {{string}} path
|
||||
* @param {{{{ onValue?: (value: Uint8Array) => void, signal?: AbortSignal }}}} [options]
|
||||
* @returns {{Promise<Uint8Array>}}
|
||||
*/
|
||||
getBytes(path, options) {{
|
||||
return this._getCached(path, async (res) => new Uint8Array(await res.arrayBuffer()), options);
|
||||
}}
|
||||
|
||||
/**
|
||||
* Make a POST request with a string body.
|
||||
*
|
||||
* POST responses are uncached and never invoke `onValue` — every call hits
|
||||
* the network with the same body and returns the upstream response.
|
||||
*
|
||||
* @param {{string}} path
|
||||
* @param {{string}} body
|
||||
* @param {{{{ signal?: AbortSignal }}}} [options]
|
||||
* @returns {{Promise<Response>}}
|
||||
*/
|
||||
async post(path, body, {{ signal }} = {{}}) {{
|
||||
const url = `${{this.baseUrl}}${{path}}`;
|
||||
const signals = [AbortSignal.timeout(this.timeout)];
|
||||
if (signal) signals.push(signal);
|
||||
const res = await fetch(url, {{
|
||||
method: 'POST',
|
||||
body,
|
||||
signal: AbortSignal.any(signals),
|
||||
}});
|
||||
if (!res.ok) throw new BrkError(`HTTP ${{res.status}}: ${{url}}`, res.status);
|
||||
return res;
|
||||
}}
|
||||
|
||||
/**
|
||||
* Make a POST request expecting a JSON response.
|
||||
* @template T
|
||||
* @param {{string}} path
|
||||
* @param {{string}} body
|
||||
* @param {{{{ signal?: AbortSignal }}}} [options]
|
||||
* @returns {{Promise<T>}}
|
||||
*/
|
||||
async postJson(path, body, options) {{
|
||||
const res = await this.post(path, body, options);
|
||||
return _addCamelGetters(await res.json());
|
||||
}}
|
||||
|
||||
/**
|
||||
* Make a POST request expecting a text response.
|
||||
* @param {{string}} path
|
||||
* @param {{string}} body
|
||||
* @param {{{{ signal?: AbortSignal }}}} [options]
|
||||
* @returns {{Promise<string>}}
|
||||
*/
|
||||
async postText(path, body, options) {{
|
||||
const res = await this.post(path, body, options);
|
||||
return res.text();
|
||||
}}
|
||||
|
||||
/**
|
||||
* Make a POST request expecting binary data (application/octet-stream).
|
||||
* @param {{string}} path
|
||||
* @param {{string}} body
|
||||
* @param {{{{ signal?: AbortSignal }}}} [options]
|
||||
* @returns {{Promise<Uint8Array>}}
|
||||
*/
|
||||
async postBytes(path, body, options) {{
|
||||
const res = await this.post(path, body, options);
|
||||
return new Uint8Array(await res.arrayBuffer());
|
||||
}}
|
||||
|
||||
/**
|
||||
* Fetch series data and wrap with helper methods (internal)
|
||||
* @template T
|
||||
* @param {{string}} path
|
||||
* @param {{(value: DateSeriesData<T>) => void}} [onValue]
|
||||
* @returns {{Promise<DateSeriesData<T>>}}
|
||||
*/
|
||||
async _fetchSeriesData(path, onValue) {{
|
||||
const wrappedOnValue = onValue ? (/** @type {{SeriesData<T>}} */ raw) => onValue(_wrapSeriesData(raw)) : undefined;
|
||||
const raw = await this.getJson(path, {{ onValue: wrappedOnValue }});
|
||||
return _wrapSeriesData(raw);
|
||||
}}
|
||||
}}
|
||||
|
||||
/**
|
||||
* Build metric name with suffix.
|
||||
* Build series name with suffix.
|
||||
* @param {{string}} acc - Accumulated prefix
|
||||
* @param {{string}} s - Metric suffix
|
||||
* @param {{string}} s - Series suffix
|
||||
* @returns {{string}}
|
||||
*/
|
||||
const _m = (acc, s) => s ? (acc ? `${{acc}}_${{s}}` : s) : acc;
|
||||
|
||||
/**
|
||||
* Build metric name with prefix.
|
||||
* Build series name with prefix.
|
||||
* @param {{string}} prefix - Prefix to prepend
|
||||
* @param {{string}} acc - Accumulated name
|
||||
* @returns {{string}}
|
||||
@@ -291,12 +678,43 @@ pub fn generate_static_constants(output: &mut String) {
|
||||
// VERSION, INDEXES, POOL_ID_TO_POOL_NAME
|
||||
writeln!(output, " VERSION = \"{}\";\n", constants.version).unwrap();
|
||||
write_static_const(output, "INDEXES", &format_json(&constants.indexes));
|
||||
write_static_const(output, "POOL_ID_TO_POOL_NAME", &format_json(&constants.pool_map));
|
||||
write_static_const(
|
||||
output,
|
||||
"POOL_ID_TO_POOL_NAME",
|
||||
&format_json(&constants.pool_map),
|
||||
);
|
||||
|
||||
// Cohort constants with camelCase keys
|
||||
for (name, value) in CohortConstants::all() {
|
||||
write_static_const(output, name, &format_json(&camel_case_keys(value)));
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
writeln!(
|
||||
output,
|
||||
r#" /**
|
||||
* Convert an index value to a Date for date-based indexes.
|
||||
* @param {{Index}} index - The index type
|
||||
* @param {{number}} i - The index value
|
||||
* @returns {{globalThis.Date}}
|
||||
*/
|
||||
indexToDate(index, i) {{
|
||||
return indexToDate(index, i);
|
||||
}}
|
||||
|
||||
/**
|
||||
* Convert a Date to an index value for date-based indexes.
|
||||
* @param {{Index}} index - The index type
|
||||
* @param {{globalThis.Date}} d - The date to convert
|
||||
* @returns {{number}}
|
||||
*/
|
||||
dateToIndex(index, d) {{
|
||||
return dateToIndex(index, d);
|
||||
}}
|
||||
|
||||
"#
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn indent_json_const(json: &str) -> String {
|
||||
@@ -331,31 +749,31 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
|
||||
|
||||
writeln!(output, "// Index group constants and factory\n").unwrap();
|
||||
|
||||
// Generate index array constants (e.g., _i1 = ["dateindex", "height"])
|
||||
// Generate index array constants (e.g., _i1 = ["day1", "height"])
|
||||
for (i, pattern) in patterns.iter().enumerate() {
|
||||
write!(output, "const _i{} = /** @type {{const}} */ ([", i + 1).unwrap();
|
||||
for (j, index) in pattern.indexes.iter().enumerate() {
|
||||
if j > 0 {
|
||||
write!(output, ", ").unwrap();
|
||||
}
|
||||
write!(output, "\"{}\"", index.serialize_long()).unwrap();
|
||||
write!(output, "\"{}\"", index.name()).unwrap();
|
||||
}
|
||||
writeln!(output, "]);").unwrap();
|
||||
}
|
||||
writeln!(output).unwrap();
|
||||
|
||||
// Generate ONE generic metric pattern factory
|
||||
// Generate ONE generic series pattern factory
|
||||
writeln!(
|
||||
output,
|
||||
r#"/**
|
||||
* Generic metric pattern factory.
|
||||
* Generic series pattern factory.
|
||||
* @template T
|
||||
* @param {{BrkClientBase}} client
|
||||
* @param {{string}} name - The metric vec name
|
||||
* @param {{BrkClient}} client
|
||||
* @param {{string}} name - The series vec name
|
||||
* @param {{readonly Index[]}} indexes - The supported indexes
|
||||
*/
|
||||
function _mp(client, name, indexes) {{
|
||||
const by = /** @type {{any}} */ ({{}});
|
||||
const by = {{}};
|
||||
for (const idx of indexes) {{
|
||||
Object.defineProperty(by, idx, {{
|
||||
get() {{ return _endpoint(client, name, idx); }},
|
||||
@@ -366,8 +784,9 @@ function _mp(client, name, indexes) {{
|
||||
return {{
|
||||
name,
|
||||
by,
|
||||
/** @returns {{readonly Index[]}} */
|
||||
indexes() {{ return indexes; }},
|
||||
/** @param {{Index}} index */
|
||||
/** @param {{Index}} index @returns {{SeriesEndpoint<T>|undefined}} */
|
||||
get(index) {{ return indexes.includes(index) ? _endpoint(client, name, index) : undefined; }}
|
||||
}};
|
||||
}}
|
||||
@@ -382,17 +801,19 @@ function _mp(client, name, indexes) {{
|
||||
.indexes
|
||||
.iter()
|
||||
.map(|idx| {
|
||||
format!(
|
||||
"readonly {}: MetricEndpointBuilder<T>",
|
||||
idx.serialize_long()
|
||||
)
|
||||
let builder = if idx.is_date_based() {
|
||||
"DateSeriesEndpoint"
|
||||
} else {
|
||||
"SeriesEndpoint"
|
||||
};
|
||||
format!("readonly {}: {}<T>", idx.name(), builder)
|
||||
})
|
||||
.collect();
|
||||
let by_type = format!("{{ {} }}", by_fields.join(", "));
|
||||
|
||||
writeln!(
|
||||
output,
|
||||
"/** @template T @typedef {{{{ name: string, by: {}, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }}}} {} */",
|
||||
"/** @template T @typedef {{{{ name: string, by: {}, indexes: () => readonly Index[], get: (index: Index) => SeriesEndpoint<T>|undefined }}}} {} */",
|
||||
by_type, pattern.name
|
||||
)
|
||||
.unwrap();
|
||||
@@ -400,13 +821,14 @@ function _mp(client, name, indexes) {{
|
||||
// Generate thin wrapper that calls the generic factory
|
||||
writeln!(
|
||||
output,
|
||||
"/** @template T @param {{BrkClientBase}} client @param {{string}} name @returns {{{}<T>}} */",
|
||||
"/** @template T @param {{BrkClient}} client @param {{string}} name @returns {{{}<T>}} */",
|
||||
pattern.name
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
"function create{}(client, name) {{ return _mp(client, name, _i{}); }}",
|
||||
"function create{}(client, name) {{ return /** @type {{{}<T>}} */ (_mp(client, name, _i{})); }}",
|
||||
pattern.name,
|
||||
pattern.name,
|
||||
i + 1
|
||||
)
|
||||
@@ -451,14 +873,21 @@ pub fn generate_structural_patterns(
|
||||
}
|
||||
writeln!(output, " */\n").unwrap();
|
||||
|
||||
// Generate factory function for ALL patterns
|
||||
// Skip factory for non-parameterizable patterns (inlined at tree level)
|
||||
if !metadata.is_parameterizable(&pattern.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
writeln!(output, "/**").unwrap();
|
||||
writeln!(output, " * Create a {} pattern node", pattern.name).unwrap();
|
||||
if pattern.is_generic {
|
||||
writeln!(output, " * @template T").unwrap();
|
||||
}
|
||||
writeln!(output, " * @param {{BrkClientBase}} client").unwrap();
|
||||
writeln!(output, " * @param {{string}} acc - Accumulated metric name").unwrap();
|
||||
writeln!(output, " * @param {{BrkClient}} client").unwrap();
|
||||
writeln!(output, " * @param {{string}} acc - Accumulated series name").unwrap();
|
||||
if pattern.is_templated() {
|
||||
writeln!(output, " * @param {{string}} disc - Discriminator suffix").unwrap();
|
||||
}
|
||||
let return_type = if pattern.is_generic {
|
||||
format!("{}<T>", pattern.name)
|
||||
} else {
|
||||
@@ -467,7 +896,16 @@ pub fn generate_structural_patterns(
|
||||
writeln!(output, " * @returns {{{}}}", return_type).unwrap();
|
||||
writeln!(output, " */").unwrap();
|
||||
|
||||
writeln!(output, "function create{}(client, acc) {{", pattern.name).unwrap();
|
||||
if pattern.is_templated() {
|
||||
writeln!(
|
||||
output,
|
||||
"function create{}(client, acc, disc) {{",
|
||||
pattern.name
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(output, "function create{}(client, acc) {{", pattern.name).unwrap();
|
||||
}
|
||||
writeln!(output, " return {{").unwrap();
|
||||
|
||||
let syntax = JavaScriptSyntax;
|
||||
|
||||
@@ -11,6 +11,7 @@ use std::{fmt::Write, fs, io, path::Path};
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use super::write_if_changed;
|
||||
use crate::{ClientMetadata, Endpoint, TypeSchemas, VERSION};
|
||||
|
||||
/// Generate JavaScript + JSDoc client from metadata and OpenAPI endpoints.
|
||||
@@ -34,7 +35,7 @@ pub fn generate_javascript_client(
|
||||
tree::generate_tree_typedefs(&mut output, &metadata.catalog, metadata);
|
||||
tree::generate_main_client(&mut output, &metadata.catalog, metadata, endpoints);
|
||||
|
||||
fs::write(output_path, output)?;
|
||||
write_if_changed(output_path, &output)?;
|
||||
|
||||
// Update package.json version if it exists in the same directory
|
||||
if let Some(parent) = output_path.parent() {
|
||||
@@ -59,7 +60,7 @@ fn update_package_json_version(package_json_path: &Path) -> io::Result<()> {
|
||||
let updated = serde_json::to_string_pretty(&package)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
|
||||
fs::write(package_json_path, updated + "\n")?;
|
||||
write_if_changed(package_json_path, &(updated + "\n"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
//! JavaScript tree structure generation.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::collections::BTreeSet;
|
||||
use std::fmt::Write;
|
||||
|
||||
use brk_types::TreeNode;
|
||||
|
||||
use crate::{
|
||||
ClientMetadata, Endpoint, GenericSyntax, JavaScriptSyntax, PatternField, build_child_path,
|
||||
generate_leaf_field, prepare_tree_node, to_camel_case,
|
||||
generate_leaf_field, generate_tree_node_field, prepare_tree_node, to_camel_case,
|
||||
};
|
||||
|
||||
use super::api::generate_api_methods;
|
||||
use super::client::generate_static_constants;
|
||||
|
||||
/// Generate JSDoc typedefs for the metrics tree.
|
||||
/// Generate JSDoc typedefs for the series tree.
|
||||
pub fn generate_tree_typedefs(output: &mut String, catalog: &TreeNode, metadata: &ClientMetadata) {
|
||||
writeln!(output, "// Catalog tree typedefs\n").unwrap();
|
||||
|
||||
let pattern_lookup = metadata.pattern_lookup();
|
||||
let mut generated = HashSet::new();
|
||||
let mut generated = BTreeSet::new();
|
||||
generate_tree_typedef(
|
||||
output,
|
||||
"MetricsTree",
|
||||
"SeriesTree",
|
||||
"",
|
||||
catalog,
|
||||
&pattern_lookup,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
&mut generated,
|
||||
);
|
||||
@@ -35,9 +35,9 @@ fn generate_tree_typedef(
|
||||
name: &str,
|
||||
path: &str,
|
||||
node: &TreeNode,
|
||||
pattern_lookup: &std::collections::HashMap<Vec<PatternField>, String>,
|
||||
pattern_lookup: &std::collections::BTreeMap<Vec<PatternField>, String>,
|
||||
metadata: &ClientMetadata,
|
||||
generated: &mut HashSet<String>,
|
||||
generated: &mut BTreeSet<String>,
|
||||
) {
|
||||
let Some(ctx) = prepare_tree_node(node, name, path, pattern_lookup, metadata, generated) else {
|
||||
return;
|
||||
@@ -50,13 +50,7 @@ fn generate_tree_typedef(
|
||||
let js_type = if child.should_inline {
|
||||
child.inline_type_name.clone()
|
||||
} else {
|
||||
metadata.resolve_tree_field_type(
|
||||
&child.field,
|
||||
child.child_fields.as_deref(),
|
||||
name,
|
||||
child.name,
|
||||
GenericSyntax::JAVASCRIPT,
|
||||
)
|
||||
metadata.field_type_annotation(&child.field, false, None, GenericSyntax::JAVASCRIPT)
|
||||
};
|
||||
|
||||
writeln!(
|
||||
@@ -99,7 +93,7 @@ pub fn generate_main_client(
|
||||
writeln!(output, "/**").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" * Main BRK client with metrics tree and API methods"
|
||||
" * Main BRK client with series tree and API methods"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " * @extends BrkClientBase").unwrap();
|
||||
@@ -113,25 +107,24 @@ pub fn generate_main_client(
|
||||
writeln!(output, " */").unwrap();
|
||||
writeln!(output, " constructor(options) {{").unwrap();
|
||||
writeln!(output, " super(options);").unwrap();
|
||||
writeln!(output, " /** @type {{MetricsTree}} */").unwrap();
|
||||
writeln!(output, " this.metrics = this._buildTree('');").unwrap();
|
||||
writeln!(output, " /** @type {{SeriesTree}} */").unwrap();
|
||||
writeln!(output, " this.series = this._buildTree();").unwrap();
|
||||
writeln!(output, " }}\n").unwrap();
|
||||
|
||||
writeln!(output, " /**").unwrap();
|
||||
writeln!(output, " * @private").unwrap();
|
||||
writeln!(output, " * @param {{string}} basePath").unwrap();
|
||||
writeln!(output, " * @returns {{MetricsTree}}").unwrap();
|
||||
writeln!(output, " * @returns {{SeriesTree}}").unwrap();
|
||||
writeln!(output, " */").unwrap();
|
||||
writeln!(output, " _buildTree(basePath) {{").unwrap();
|
||||
writeln!(output, " _buildTree() {{").unwrap();
|
||||
writeln!(output, " return {{").unwrap();
|
||||
let mut generated = HashSet::new();
|
||||
let mut generated = BTreeSet::new();
|
||||
generate_tree_initializer(
|
||||
output,
|
||||
catalog,
|
||||
"MetricsTree",
|
||||
"SeriesTree",
|
||||
"",
|
||||
3,
|
||||
&pattern_lookup,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
&mut generated,
|
||||
);
|
||||
@@ -141,27 +134,27 @@ pub fn generate_main_client(
|
||||
writeln!(output, " /**").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" * Create a dynamic metric endpoint builder for any metric/index combination."
|
||||
" * Create a dynamic series endpoint builder for any series/index combination."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " *").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" * Use this for programmatic access when the metric name is determined at runtime."
|
||||
" * Use this for programmatic access when the series name is determined at runtime."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" * For type-safe access, use the `metrics` tree instead."
|
||||
" * For type-safe access, use the `series` tree instead."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " *").unwrap();
|
||||
writeln!(output, " * @param {{string}} metric - The metric name").unwrap();
|
||||
writeln!(output, " * @param {{string}} series - The series name").unwrap();
|
||||
writeln!(output, " * @param {{Index}} index - The index name").unwrap();
|
||||
writeln!(output, " * @returns {{MetricEndpointBuilder<unknown>}}").unwrap();
|
||||
writeln!(output, " * @returns {{SeriesEndpoint<unknown>}}").unwrap();
|
||||
writeln!(output, " */").unwrap();
|
||||
writeln!(output, " metric(metric, index) {{").unwrap();
|
||||
writeln!(output, " return _endpoint(this, metric, index);").unwrap();
|
||||
writeln!(output, " seriesEndpoint(series, index) {{").unwrap();
|
||||
writeln!(output, " return _endpoint(this, series, index);").unwrap();
|
||||
writeln!(output, " }}\n").unwrap();
|
||||
|
||||
generate_api_methods(output, endpoints);
|
||||
@@ -178,9 +171,9 @@ fn generate_tree_initializer(
|
||||
name: &str,
|
||||
path: &str,
|
||||
indent: usize,
|
||||
pattern_lookup: &std::collections::HashMap<Vec<PatternField>, String>,
|
||||
pattern_lookup: &std::collections::BTreeMap<Vec<PatternField>, String>,
|
||||
metadata: &ClientMetadata,
|
||||
generated: &mut HashSet<String>,
|
||||
generated: &mut BTreeSet<String>,
|
||||
) {
|
||||
let indent_str = " ".repeat(indent);
|
||||
|
||||
@@ -220,13 +213,15 @@ fn generate_tree_initializer(
|
||||
);
|
||||
writeln!(output, "{}}},", indent_str).unwrap();
|
||||
} else {
|
||||
// Use pattern factory
|
||||
writeln!(
|
||||
generate_tree_node_field(
|
||||
output,
|
||||
"{}{}: create{}(this, '{}'),",
|
||||
indent_str, field_name, child.field.rust_type, child.base_result.base
|
||||
)
|
||||
.unwrap();
|
||||
&syntax,
|
||||
&child.field,
|
||||
metadata,
|
||||
&indent_str,
|
||||
"this",
|
||||
&child.base_result,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@ use std::fmt::Write;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{TypeSchemas, generators::{MANUAL_GENERIC_TYPES, write_description}, get_union_variants, ref_to_type_name, to_camel_case};
|
||||
use crate::{
|
||||
TypeSchemas,
|
||||
generators::{MANUAL_GENERIC_TYPES, write_description},
|
||||
get_union_variants, ref_to_type_name, to_camel_case,
|
||||
};
|
||||
|
||||
/// Generate JSDoc type definitions from OpenAPI schemas.
|
||||
pub fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
|
||||
@@ -48,7 +52,7 @@ pub fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
|
||||
.map(|arr| arr.iter().any(|v| v.as_str() == Some(prop_name)))
|
||||
.unwrap_or(false);
|
||||
let optional = if required { "" } else { "=" };
|
||||
let safe_name = to_camel_case(prop_name);
|
||||
let safe_name = to_camel_case(&prop_name.replace(['[', ']'], ""));
|
||||
let prop_desc = prop_schema
|
||||
.get("description")
|
||||
.and_then(|d| d.as_str())
|
||||
@@ -107,6 +111,25 @@ fn json_type_to_js(ty: &str, schema: &Value, current_type: Option<&str>) -> Stri
|
||||
}
|
||||
}
|
||||
|
||||
/// JSDoc has no `integer` keyword, only `number`. Map `integer` (and `integer[]`,
|
||||
/// `Foo<integer>`, etc.) to `number` before emitting type strings to JS.
|
||||
pub fn jsdoc_normalize(ty: &str) -> String {
|
||||
let mut out = ty.to_string();
|
||||
let mut prev = String::new();
|
||||
while prev != out {
|
||||
prev = out.clone();
|
||||
out = out.replace("integer[]", "number[]");
|
||||
out = out.replace("<integer>", "<number>");
|
||||
out = out.replace("(integer)", "(number)");
|
||||
out = out.replace("integer | ", "number | ");
|
||||
out = out.replace(" | integer", " | number");
|
||||
}
|
||||
if out == "integer" {
|
||||
return "number".to_string();
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Convert a JSON schema to a JavaScript type string.
|
||||
pub fn schema_to_js_type(schema: &Value, current_type: Option<&str>) -> String {
|
||||
if let Some(all_of) = schema.get("allOf").and_then(|v| v.as_array()) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! - `api.rs` - API method generation
|
||||
//! - `mod.rs` - Entry point
|
||||
|
||||
use std::fmt::Write;
|
||||
use std::{fmt::Write, fs, io, path::Path};
|
||||
|
||||
pub mod javascript;
|
||||
pub mod python;
|
||||
@@ -18,7 +18,7 @@ pub use python::generate_python_client;
|
||||
pub use rust::generate_rust_client;
|
||||
|
||||
/// Types that are manually defined as generics in client code, not from schema.
|
||||
pub const MANUAL_GENERIC_TYPES: &[&str] = &["MetricData", "MetricEndpoint"];
|
||||
pub const MANUAL_GENERIC_TYPES: &[&str] = &["SeriesData", "SeriesEndpoint"];
|
||||
|
||||
/// Write a multi-line description with the given prefix for each line.
|
||||
/// `empty_prefix` is used for blank lines (e.g., " *" without trailing space).
|
||||
@@ -41,3 +41,14 @@ pub fn normalize_return_type(return_type: &str) -> String {
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Write content to a file only if it differs from existing content.
|
||||
/// Preserves mtime when unchanged, avoiding unnecessary cargo rebuilds.
|
||||
pub fn write_if_changed(path: &Path, content: &str) -> io::Result<()> {
|
||||
if let Ok(existing) = fs::read_to_string(path)
|
||||
&& existing == content
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
fs::write(path, content)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
use crate::{Endpoint, Parameter, escape_python_keyword, generators::{normalize_return_type, write_description}, to_snake_case};
|
||||
use crate::{
|
||||
Endpoint, Parameter, escape_python_keyword,
|
||||
generators::{normalize_return_type, write_description},
|
||||
to_snake_case,
|
||||
};
|
||||
|
||||
use super::client::generate_class_constants;
|
||||
use super::types::js_type_to_python;
|
||||
@@ -12,7 +16,7 @@ pub fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) {
|
||||
writeln!(output, "class BrkClient(BrkClientBase):").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" \"\"\"Main BRK client with metrics tree and API methods.\"\"\""
|
||||
" \"\"\"Main BRK client with series tree and API methods.\"\"\""
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output).unwrap();
|
||||
@@ -26,19 +30,60 @@ pub fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) {
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " super().__init__(base_url, timeout)").unwrap();
|
||||
writeln!(output, " self.metrics = MetricsTree(self)").unwrap();
|
||||
writeln!(output, " self.series = SeriesTree(self)").unwrap();
|
||||
writeln!(output).unwrap();
|
||||
|
||||
// Generate metric() method for dynamic metric access
|
||||
writeln!(output, " def metric(self, metric: str, index: Index) -> MetricEndpointBuilder[Any]:").unwrap();
|
||||
writeln!(output, " \"\"\"Create a dynamic metric endpoint builder for any metric/index combination.").unwrap();
|
||||
// Generate series_endpoint() method for dynamic series access
|
||||
writeln!(
|
||||
output,
|
||||
" def series_endpoint(self, series: str, index: Index) -> SeriesEndpoint[Any]:"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" \"\"\"Create a dynamic series endpoint builder for any series/index combination."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output).unwrap();
|
||||
writeln!(output, " Use this for programmatic access when the metric name is determined at runtime.").unwrap();
|
||||
writeln!(output, " For type-safe access, use the `metrics` tree instead.").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" Use this for programmatic access when the series name is determined at runtime."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" For type-safe access, use the `series` tree instead."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " \"\"\"").unwrap();
|
||||
writeln!(output, " return MetricEndpointBuilder(self, metric, index)").unwrap();
|
||||
writeln!(output, " return SeriesEndpoint(self, series, index)").unwrap();
|
||||
writeln!(output).unwrap();
|
||||
|
||||
// Generate helper methods
|
||||
writeln!(
|
||||
output,
|
||||
" def index_to_date(self, index: Index, i: int) -> Union[date, datetime]:"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" \"\"\"Convert an index value to a date/datetime for date-based indexes.\"\"\""
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " return _index_to_date(index, i)").unwrap();
|
||||
writeln!(output).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" def date_to_index(self, index: Index, d: Union[date, datetime]) -> int:"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" \"\"\"Convert a date/datetime to an index value for date-based indexes.\"\"\""
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " return _date_to_index(index, d)").unwrap();
|
||||
writeln!(output).unwrap();
|
||||
// Generate API methods
|
||||
generate_api_methods(output, endpoints);
|
||||
}
|
||||
@@ -51,13 +96,16 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
}
|
||||
|
||||
let method_name = endpoint_to_method_name(endpoint);
|
||||
let base_return_type = normalize_return_type(
|
||||
&endpoint
|
||||
.response_type
|
||||
.as_deref()
|
||||
.map(js_type_to_python)
|
||||
.unwrap_or_else(|| "Any".to_string()),
|
||||
);
|
||||
let base_return_type = if endpoint.returns_binary() {
|
||||
"bytes".to_string()
|
||||
} else {
|
||||
normalize_return_type(
|
||||
&endpoint
|
||||
.schema_name()
|
||||
.map(js_type_to_python)
|
||||
.unwrap_or_else(|| "str".to_string()),
|
||||
)
|
||||
};
|
||||
|
||||
let return_type = if endpoint.supports_csv {
|
||||
format!("Union[{}, str]", base_return_type)
|
||||
@@ -103,23 +151,69 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
}
|
||||
}
|
||||
writeln!(output).unwrap();
|
||||
writeln!(output, " Endpoint: `{} {}`\"\"\"", endpoint.method.to_uppercase(), endpoint.path).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" Endpoint: `{} {}`\"\"\"",
|
||||
endpoint.method.to_uppercase(),
|
||||
endpoint.path
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Build path
|
||||
let path = build_path_template(&endpoint.path, &endpoint.path_params);
|
||||
|
||||
let is_post = endpoint.method == "POST";
|
||||
let fetch_method = match (is_post, &endpoint.response_kind) {
|
||||
(false, _) if endpoint.returns_binary() => "get",
|
||||
(false, _) if endpoint.returns_json() => "get_json",
|
||||
(false, _) => "get_text",
|
||||
(true, _) if endpoint.returns_binary() => "post",
|
||||
(true, _) if endpoint.returns_json() => "post_json",
|
||||
(true, _) => "post_text",
|
||||
};
|
||||
|
||||
let body_arg = if is_post && endpoint.request_body.is_some() {
|
||||
", body"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let (wrap_prefix, wrap_suffix) = if endpoint.response_kind.text_is_numeric() {
|
||||
("int(", ")")
|
||||
} else {
|
||||
("", "")
|
||||
};
|
||||
|
||||
if endpoint.query_params.is_empty() {
|
||||
if endpoint.path_params.is_empty() {
|
||||
writeln!(output, " return self.get_json('{}')", path).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" return {}self.{}('{}'{}){}",
|
||||
wrap_prefix, fetch_method, path, body_arg, wrap_suffix
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(output, " return self.get_json(f'{}')", path).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" return {}self.{}(f'{}'{}){}",
|
||||
wrap_prefix, fetch_method, path, body_arg, wrap_suffix
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
} else {
|
||||
writeln!(output, " params = []").unwrap();
|
||||
for param in &endpoint.query_params {
|
||||
// Use safe name for Python variable, original name for API query parameter
|
||||
let safe_name = escape_python_keyword(¶m.name);
|
||||
if param.required {
|
||||
let is_array = param.param_type.ends_with("[]");
|
||||
if is_array {
|
||||
writeln!(
|
||||
output,
|
||||
" for _v in {}: params.append(f'{}={{_v}}')",
|
||||
safe_name, param.name
|
||||
)
|
||||
.unwrap();
|
||||
} else if param.required {
|
||||
writeln!(
|
||||
output,
|
||||
" params.append(f'{}={{{}}}')",
|
||||
@@ -146,9 +240,19 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
if endpoint.supports_csv {
|
||||
writeln!(output, " if format == 'csv':").unwrap();
|
||||
writeln!(output, " return self.get_text(path)").unwrap();
|
||||
writeln!(output, " return self.get_json(path)").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" return {}self.{}(path{}){}",
|
||||
wrap_prefix, fetch_method, body_arg, wrap_suffix
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(output, " return self.get_json(path)").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" return {}self.{}(path{}){}",
|
||||
wrap_prefix, fetch_method, body_arg, wrap_suffix
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,6 +287,14 @@ fn build_method_params(endpoint: &Endpoint) -> String {
|
||||
params.push(format!(", {}: Optional[{}] = None", safe_name, py_type));
|
||||
}
|
||||
}
|
||||
if let Some(body) = &endpoint.request_body {
|
||||
let py_type = js_type_to_python(&body.body_type);
|
||||
if body.required {
|
||||
params.push(format!(", body: {}", py_type));
|
||||
} else {
|
||||
params.push(format!(", body: Optional[{}] = None", py_type));
|
||||
}
|
||||
}
|
||||
params.join("")
|
||||
}
|
||||
|
||||
|
||||
@@ -99,27 +99,49 @@ class BrkClientBase:
|
||||
"""Make a GET request and return text."""
|
||||
return self.get(path).decode()
|
||||
|
||||
def close(self):
|
||||
def post(self, path: str, body: str) -> bytes:
|
||||
"""Make a POST request with a string body and return raw bytes."""
|
||||
try:
|
||||
conn = self._connect()
|
||||
conn.request("POST", path, body=body)
|
||||
res = conn.getresponse()
|
||||
data = res.read()
|
||||
if res.status >= 400:
|
||||
raise BrkError(f"HTTP error: {{res.status}}", res.status)
|
||||
return data
|
||||
except (ConnectionError, OSError, TimeoutError) as e:
|
||||
self._conn = None
|
||||
raise BrkError(str(e))
|
||||
|
||||
def post_json(self, path: str, body: str) -> Any:
|
||||
"""Make a POST request and return JSON."""
|
||||
return json.loads(self.post(path, body))
|
||||
|
||||
def post_text(self, path: str, body: str) -> str:
|
||||
"""Make a POST request and return text."""
|
||||
return self.post(path, body).decode()
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the HTTP client."""
|
||||
if self._conn:
|
||||
self._conn.close()
|
||||
self._conn = None
|
||||
|
||||
def __enter__(self):
|
||||
def __enter__(self) -> BrkClientBase:
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
def __exit__(self, exc_type: Optional[type], exc_val: Optional[BaseException], exc_tb: Optional[Any]) -> None:
|
||||
self.close()
|
||||
|
||||
|
||||
def _m(acc: str, s: str) -> str:
|
||||
"""Build metric name with suffix."""
|
||||
"""Build series name with suffix."""
|
||||
if not s: return acc
|
||||
return f"{{acc}}_{{s}}" if acc else s
|
||||
|
||||
|
||||
def _p(prefix: str, acc: str) -> str:
|
||||
"""Build metric name with prefix."""
|
||||
"""Build series name with prefix."""
|
||||
return f"{{prefix}}_{{acc}}" if acc else prefix
|
||||
|
||||
"#
|
||||
@@ -127,31 +149,211 @@ def _p(prefix: str, acc: str) -> str:
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Generate the MetricData and MetricEndpointBuilder classes
|
||||
/// Generate the SeriesData and SeriesEndpoint classes
|
||||
pub fn generate_endpoint_class(output: &mut String) {
|
||||
writeln!(
|
||||
output,
|
||||
r#"class MetricData(TypedDict, Generic[T]):
|
||||
"""Metric data with range information."""
|
||||
total: int
|
||||
r#"# Date conversion constants
|
||||
_GENESIS = date(2009, 1, 3) # day1 0, week1 0
|
||||
_DAY_ONE = date(2009, 1, 9) # day1 1 (6 day gap after genesis)
|
||||
_EPOCH = datetime(2009, 1, 1, tzinfo=timezone.utc)
|
||||
_DATE_INDEXES = frozenset([
|
||||
'minute10', 'minute30',
|
||||
'hour1', 'hour4', 'hour12',
|
||||
'day1', 'day3', 'week1',
|
||||
'month1', 'month3', 'month6',
|
||||
'year1', 'year10',
|
||||
])
|
||||
|
||||
def _index_to_date(index: str, i: int) -> Union[date, datetime]:
|
||||
"""Convert an index value to a date/datetime for date-based indexes."""
|
||||
if index == 'minute10':
|
||||
return _EPOCH + timedelta(minutes=i * 10)
|
||||
elif index == 'minute30':
|
||||
return _EPOCH + timedelta(minutes=i * 30)
|
||||
elif index == 'hour1':
|
||||
return _EPOCH + timedelta(hours=i)
|
||||
elif index == 'hour4':
|
||||
return _EPOCH + timedelta(hours=i * 4)
|
||||
elif index == 'hour12':
|
||||
return _EPOCH + timedelta(hours=i * 12)
|
||||
elif index == 'day1':
|
||||
return _GENESIS if i == 0 else _DAY_ONE + timedelta(days=i - 1)
|
||||
elif index == 'day3':
|
||||
return _EPOCH.date() - timedelta(days=1) + timedelta(days=i * 3)
|
||||
elif index == 'week1':
|
||||
return _GENESIS + timedelta(weeks=i)
|
||||
elif index == 'month1':
|
||||
return date(2009 + i // 12, i % 12 + 1, 1)
|
||||
elif index == 'month3':
|
||||
m = i * 3
|
||||
return date(2009 + m // 12, m % 12 + 1, 1)
|
||||
elif index == 'month6':
|
||||
m = i * 6
|
||||
return date(2009 + m // 12, m % 12 + 1, 1)
|
||||
elif index == 'year1':
|
||||
return date(2009 + i, 1, 1)
|
||||
elif index == 'year10':
|
||||
return date(2009 + i * 10, 1, 1)
|
||||
else:
|
||||
raise ValueError(f"{{index}} is not a date-based index")
|
||||
|
||||
|
||||
def _date_to_index(index: str, d: Union[date, datetime]) -> int:
|
||||
"""Convert a date/datetime to an index value for date-based indexes.
|
||||
|
||||
Returns the floor index (latest index whose date is <= the given date).
|
||||
For sub-day indexes (minute*, hour*), a plain date is treated as midnight UTC.
|
||||
"""
|
||||
if index in ('minute10', 'minute30', 'hour1', 'hour4', 'hour12'):
|
||||
if isinstance(d, datetime):
|
||||
dt = d if d.tzinfo else d.replace(tzinfo=timezone.utc)
|
||||
else:
|
||||
dt = datetime(d.year, d.month, d.day, tzinfo=timezone.utc)
|
||||
secs = int((dt - _EPOCH).total_seconds())
|
||||
div = {{'minute10': 600, 'minute30': 1800,
|
||||
'hour1': 3600, 'hour4': 14400, 'hour12': 43200}}
|
||||
return secs // div[index]
|
||||
dd = d.date() if isinstance(d, datetime) else d
|
||||
if index == 'day1':
|
||||
if dd < _DAY_ONE:
|
||||
return 0
|
||||
return 1 + (dd - _DAY_ONE).days
|
||||
elif index == 'day3':
|
||||
return (dd - date(2008, 12, 31)).days // 3
|
||||
elif index == 'week1':
|
||||
return (dd - _GENESIS).days // 7
|
||||
elif index == 'month1':
|
||||
return (dd.year - 2009) * 12 + (dd.month - 1)
|
||||
elif index == 'month3':
|
||||
return (dd.year - 2009) * 4 + (dd.month - 1) // 3
|
||||
elif index == 'month6':
|
||||
return (dd.year - 2009) * 2 + (dd.month - 1) // 6
|
||||
elif index == 'year1':
|
||||
return dd.year - 2009
|
||||
elif index == 'year10':
|
||||
return (dd.year - 2009) // 10
|
||||
else:
|
||||
raise ValueError(f"{{index}} is not a date-based index")
|
||||
|
||||
|
||||
@dataclass
|
||||
class SeriesData(Generic[T]):
|
||||
"""Series data with range information. Always int-indexed."""
|
||||
version: int
|
||||
index: Index
|
||||
type: str
|
||||
start: int
|
||||
end: int
|
||||
stamp: str
|
||||
data: List[T]
|
||||
|
||||
@property
|
||||
def is_date_based(self) -> bool:
|
||||
"""Whether this series uses a date-based index."""
|
||||
return self.index in _DATE_INDEXES
|
||||
|
||||
# Type alias for non-generic usage
|
||||
AnyMetricData = MetricData[Any]
|
||||
def indexes(self) -> List[int]:
|
||||
"""Get raw index numbers."""
|
||||
return list(range(self.start, self.end))
|
||||
|
||||
def keys(self) -> List[int]:
|
||||
"""Get keys as index numbers."""
|
||||
return self.indexes()
|
||||
|
||||
def items(self) -> List[Tuple[int, T]]:
|
||||
"""Get (index, value) pairs."""
|
||||
return list(zip(self.indexes(), self.data))
|
||||
|
||||
def to_dict(self) -> Dict[int, T]:
|
||||
"""Return {{index: value}} dict."""
|
||||
return dict(zip(self.indexes(), self.data))
|
||||
|
||||
def __iter__(self) -> Iterator[Tuple[int, T]]:
|
||||
"""Iterate over (index, value) pairs."""
|
||||
return iter(zip(self.indexes(), self.data))
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.data)
|
||||
|
||||
def to_polars(self) -> pl.DataFrame:
|
||||
"""Convert to Polars DataFrame with 'index' and 'value' columns."""
|
||||
try:
|
||||
import polars as pl # type: ignore[import-not-found]
|
||||
except ImportError:
|
||||
raise ImportError("polars is required: pip install polars")
|
||||
return pl.DataFrame({{"index": self.indexes(), "value": self.data}})
|
||||
|
||||
def to_pandas(self) -> pd.DataFrame:
|
||||
"""Convert to Pandas DataFrame with 'index' and 'value' columns."""
|
||||
try:
|
||||
import pandas as pd # type: ignore[import-not-found]
|
||||
except ImportError:
|
||||
raise ImportError("pandas is required: pip install pandas")
|
||||
return pd.DataFrame({{"index": self.indexes(), "value": self.data}})
|
||||
|
||||
|
||||
@dataclass
|
||||
class DateSeriesData(SeriesData[T]):
|
||||
"""Series data with date-based index. Extends SeriesData with date methods."""
|
||||
|
||||
def dates(self) -> List[Union[date, datetime]]:
|
||||
"""Get dates for the index range. Returns datetime for sub-daily indexes, date for daily+."""
|
||||
return [_index_to_date(self.index, i) for i in range(self.start, self.end)]
|
||||
|
||||
def date_items(self) -> List[Tuple[Union[date, datetime], T]]:
|
||||
"""Get (date, value) pairs."""
|
||||
return list(zip(self.dates(), self.data))
|
||||
|
||||
def to_date_dict(self) -> Dict[Union[date, datetime], T]:
|
||||
"""Return {{date: value}} dict."""
|
||||
return dict(zip(self.dates(), self.data))
|
||||
|
||||
def to_polars(self, with_dates: bool = True) -> pl.DataFrame:
|
||||
"""Convert to Polars DataFrame.
|
||||
|
||||
Returns a DataFrame with columns:
|
||||
- 'date' and 'value' if with_dates=True (default)
|
||||
- 'index' and 'value' otherwise
|
||||
"""
|
||||
try:
|
||||
import polars as pl # type: ignore[import-not-found]
|
||||
except ImportError:
|
||||
raise ImportError("polars is required: pip install polars")
|
||||
if with_dates:
|
||||
return pl.DataFrame({{"date": self.dates(), "value": self.data}})
|
||||
return pl.DataFrame({{"index": self.indexes(), "value": self.data}})
|
||||
|
||||
def to_pandas(self, with_dates: bool = True) -> pd.DataFrame:
|
||||
"""Convert to Pandas DataFrame.
|
||||
|
||||
Returns a DataFrame with columns:
|
||||
- 'date' and 'value' if with_dates=True (default)
|
||||
- 'index' and 'value' otherwise
|
||||
"""
|
||||
try:
|
||||
import pandas as pd # type: ignore[import-not-found]
|
||||
except ImportError:
|
||||
raise ImportError("pandas is required: pip install pandas")
|
||||
if with_dates:
|
||||
return pd.DataFrame({{"date": self.dates(), "value": self.data}})
|
||||
return pd.DataFrame({{"index": self.indexes(), "value": self.data}})
|
||||
|
||||
|
||||
# Type aliases for non-generic usage
|
||||
AnySeriesData = SeriesData[Any]
|
||||
AnyDateSeriesData = DateSeriesData[Any]
|
||||
|
||||
|
||||
class _EndpointConfig:
|
||||
"""Shared endpoint configuration."""
|
||||
client: BrkClientBase
|
||||
client: BrkClient
|
||||
name: str
|
||||
index: Index
|
||||
start: Optional[int]
|
||||
end: Optional[int]
|
||||
|
||||
def __init__(self, client: BrkClientBase, name: str, index: Index,
|
||||
def __init__(self, client: BrkClient, name: str, index: Index,
|
||||
start: Optional[int] = None, end: Optional[int] = None):
|
||||
self.client = client
|
||||
self.name = name
|
||||
@@ -160,7 +362,7 @@ class _EndpointConfig:
|
||||
self.end = end
|
||||
|
||||
def path(self) -> str:
|
||||
return f"/api/metric/{{self.name}}/{{self.index}}"
|
||||
return f"/api/series/{{self.name}}/{{self.index}}"
|
||||
|
||||
def _build_path(self, format: Optional[str] = None) -> str:
|
||||
params = []
|
||||
@@ -174,12 +376,24 @@ class _EndpointConfig:
|
||||
p = self.path()
|
||||
return f"{{p}}?{{query}}" if query else p
|
||||
|
||||
def get_json(self) -> Any:
|
||||
return self.client.get_json(self._build_path())
|
||||
def _new(self, start: Optional[int] = None, end: Optional[int] = None) -> _EndpointConfig:
|
||||
return _EndpointConfig(self.client, self.name, self.index, start, end)
|
||||
|
||||
def get_series(self) -> SeriesData[Any]:
|
||||
return SeriesData(**self.client.get_json(self._build_path()))
|
||||
|
||||
def get_date_series(self) -> DateSeriesData[Any]:
|
||||
return DateSeriesData(**self.client.get_json(self._build_path()))
|
||||
|
||||
def get_csv(self) -> str:
|
||||
return self.client.get_text(self._build_path(format='csv'))
|
||||
|
||||
def get_len(self) -> int:
|
||||
return self.client.get_series_len(self.name, self.index)
|
||||
|
||||
def get_version(self) -> Version:
|
||||
return self.client.get_series_version(self.name, self.index)
|
||||
|
||||
|
||||
class RangeBuilder(Generic[T]):
|
||||
"""Builder with range specified."""
|
||||
@@ -187,9 +401,9 @@ class RangeBuilder(Generic[T]):
|
||||
def __init__(self, config: _EndpointConfig):
|
||||
self._config = config
|
||||
|
||||
def fetch(self) -> MetricData[T]:
|
||||
def fetch(self) -> SeriesData[T]:
|
||||
"""Fetch the range as parsed JSON."""
|
||||
return self._config.get_json()
|
||||
return self._config.get_series()
|
||||
|
||||
def fetch_csv(self) -> str:
|
||||
"""Fetch the range as CSV string."""
|
||||
@@ -202,9 +416,9 @@ class SingleItemBuilder(Generic[T]):
|
||||
def __init__(self, config: _EndpointConfig):
|
||||
self._config = config
|
||||
|
||||
def fetch(self) -> MetricData[T]:
|
||||
def fetch(self) -> SeriesData[T]:
|
||||
"""Fetch the single item."""
|
||||
return self._config.get_json()
|
||||
return self._config.get_series()
|
||||
|
||||
def fetch_csv(self) -> str:
|
||||
"""Fetch as CSV."""
|
||||
@@ -220,47 +434,50 @@ class SkippedBuilder(Generic[T]):
|
||||
def take(self, n: int) -> RangeBuilder[T]:
|
||||
"""Take n items after the skipped position."""
|
||||
start = self._config.start or 0
|
||||
return RangeBuilder(_EndpointConfig(
|
||||
self._config.client, self._config.name, self._config.index,
|
||||
start, start + n
|
||||
))
|
||||
return RangeBuilder(self._config._new(start, start + n))
|
||||
|
||||
def fetch(self) -> MetricData[T]:
|
||||
def fetch(self) -> SeriesData[T]:
|
||||
"""Fetch from skipped position to end."""
|
||||
return self._config.get_json()
|
||||
return self._config.get_series()
|
||||
|
||||
def fetch_csv(self) -> str:
|
||||
"""Fetch as CSV."""
|
||||
return self._config.get_csv()
|
||||
|
||||
|
||||
class MetricEndpointBuilder(Generic[T]):
|
||||
"""Builder for metric endpoint queries.
|
||||
class DateRangeBuilder(RangeBuilder[T]):
|
||||
"""Range builder that returns DateSeriesData."""
|
||||
def fetch(self) -> DateSeriesData[T]:
|
||||
return self._config.get_date_series()
|
||||
|
||||
Use method chaining to specify the data range, then call fetch() or fetch_csv() to execute.
|
||||
|
||||
class DateSingleItemBuilder(SingleItemBuilder[T]):
|
||||
"""Single item builder that returns DateSeriesData."""
|
||||
def fetch(self) -> DateSeriesData[T]:
|
||||
return self._config.get_date_series()
|
||||
|
||||
|
||||
class DateSkippedBuilder(SkippedBuilder[T]):
|
||||
"""Skipped builder that returns DateSeriesData."""
|
||||
def take(self, n: int) -> DateRangeBuilder[T]:
|
||||
start = self._config.start or 0
|
||||
return DateRangeBuilder(self._config._new(start, start + n))
|
||||
def fetch(self) -> DateSeriesData[T]:
|
||||
return self._config.get_date_series()
|
||||
|
||||
|
||||
class SeriesEndpoint(Generic[T]):
|
||||
"""Builder for series endpoint queries with int-based indexing.
|
||||
|
||||
Examples:
|
||||
# Fetch all data
|
||||
data = endpoint.fetch()
|
||||
|
||||
# Single item access
|
||||
data = endpoint[5].fetch()
|
||||
|
||||
# Slice syntax (Python-native)
|
||||
data = endpoint[:10].fetch() # First 10
|
||||
data = endpoint[-5:].fetch() # Last 5
|
||||
data = endpoint[100:110].fetch() # Range
|
||||
|
||||
# Convenience methods (pandas-style)
|
||||
data = endpoint.head().fetch() # First 10 (default)
|
||||
data = endpoint.head(20).fetch() # First 20
|
||||
data = endpoint.tail(5).fetch() # Last 5
|
||||
|
||||
# Iterator-style chaining
|
||||
data = endpoint[:10].fetch()
|
||||
data = endpoint.head(20).fetch()
|
||||
data = endpoint.skip(100).take(10).fetch()
|
||||
"""
|
||||
|
||||
def __init__(self, client: BrkClientBase, name: str, index: Index):
|
||||
def __init__(self, client: BrkClient, name: str, index: Index):
|
||||
self._config = _EndpointConfig(client, name, index)
|
||||
|
||||
@overload
|
||||
@@ -269,76 +486,133 @@ class MetricEndpointBuilder(Generic[T]):
|
||||
def __getitem__(self, key: slice) -> RangeBuilder[T]: ...
|
||||
|
||||
def __getitem__(self, key: Union[int, slice]) -> Union[SingleItemBuilder[T], RangeBuilder[T]]:
|
||||
"""Access single item or slice.
|
||||
|
||||
Examples:
|
||||
endpoint[5] # Single item at index 5
|
||||
endpoint[:10] # First 10
|
||||
endpoint[-5:] # Last 5
|
||||
endpoint[100:110] # Range 100-109
|
||||
"""
|
||||
"""Access single item or slice by integer index."""
|
||||
if isinstance(key, int):
|
||||
return SingleItemBuilder(_EndpointConfig(
|
||||
self._config.client, self._config.name, self._config.index,
|
||||
key, key + 1
|
||||
))
|
||||
return RangeBuilder(_EndpointConfig(
|
||||
self._config.client, self._config.name, self._config.index,
|
||||
key.start, key.stop
|
||||
))
|
||||
return SingleItemBuilder(self._config._new(key, key + 1))
|
||||
return RangeBuilder(self._config._new(key.start, key.stop))
|
||||
|
||||
def head(self, n: int = 10) -> RangeBuilder[T]:
|
||||
"""Get the first n items (pandas-style)."""
|
||||
return RangeBuilder(_EndpointConfig(
|
||||
self._config.client, self._config.name, self._config.index,
|
||||
None, n
|
||||
))
|
||||
"""Get the first n items."""
|
||||
return RangeBuilder(self._config._new(end=n))
|
||||
|
||||
def tail(self, n: int = 10) -> RangeBuilder[T]:
|
||||
"""Get the last n items (pandas-style)."""
|
||||
start, end = (None, 0) if n == 0 else (-n, None)
|
||||
return RangeBuilder(_EndpointConfig(
|
||||
self._config.client, self._config.name, self._config.index,
|
||||
start, end
|
||||
))
|
||||
"""Get the last n items."""
|
||||
return RangeBuilder(self._config._new(end=0) if n == 0 else self._config._new(start=-n))
|
||||
|
||||
def skip(self, n: int) -> SkippedBuilder[T]:
|
||||
"""Skip the first n items. Chain with take() to get a range."""
|
||||
return SkippedBuilder(_EndpointConfig(
|
||||
self._config.client, self._config.name, self._config.index,
|
||||
n, None
|
||||
))
|
||||
"""Skip the first n items."""
|
||||
return SkippedBuilder(self._config._new(start=n))
|
||||
|
||||
def fetch(self) -> MetricData[T]:
|
||||
"""Fetch all data as parsed JSON."""
|
||||
return self._config.get_json()
|
||||
def fetch(self) -> SeriesData[T]:
|
||||
"""Fetch all data."""
|
||||
return self._config.get_series()
|
||||
|
||||
def fetch_csv(self) -> str:
|
||||
"""Fetch all data as CSV string."""
|
||||
"""Fetch all data as CSV."""
|
||||
return self._config.get_csv()
|
||||
|
||||
def len(self) -> int:
|
||||
"""Total number of data points for this series."""
|
||||
return self._config.get_len()
|
||||
|
||||
def version(self) -> Version:
|
||||
"""Current version of the series."""
|
||||
return self._config.get_version()
|
||||
|
||||
def path(self) -> str:
|
||||
"""Get the base endpoint path."""
|
||||
return self._config.path()
|
||||
|
||||
|
||||
# Type alias for non-generic usage
|
||||
AnyMetricEndpointBuilder = MetricEndpointBuilder[Any]
|
||||
class DateSeriesEndpoint(Generic[T]):
|
||||
"""Builder for series endpoint queries with date-based indexing.
|
||||
|
||||
Accepts dates in __getitem__ and returns DateSeriesData from fetch().
|
||||
|
||||
Examples:
|
||||
data = endpoint.fetch()
|
||||
data = endpoint[date(2020, 1, 1)].fetch()
|
||||
data = endpoint[date(2020, 1, 1):date(2023, 1, 1)].fetch()
|
||||
data = endpoint[:10].fetch()
|
||||
"""
|
||||
|
||||
def __init__(self, client: BrkClient, name: str, index: Index):
|
||||
self._config = _EndpointConfig(client, name, index)
|
||||
|
||||
@overload
|
||||
def __getitem__(self, key: int) -> DateSingleItemBuilder[T]: ...
|
||||
@overload
|
||||
def __getitem__(self, key: datetime) -> DateSingleItemBuilder[T]: ...
|
||||
@overload
|
||||
def __getitem__(self, key: date) -> DateSingleItemBuilder[T]: ...
|
||||
@overload
|
||||
def __getitem__(self, key: slice) -> DateRangeBuilder[T]: ...
|
||||
|
||||
def __getitem__(self, key: Union[int, slice, date, datetime]) -> Union[DateSingleItemBuilder[T], DateRangeBuilder[T]]:
|
||||
"""Access single item or slice. Accepts int, date, or datetime."""
|
||||
if isinstance(key, (date, datetime)):
|
||||
idx = _date_to_index(self._config.index, key)
|
||||
return DateSingleItemBuilder(self._config._new(idx, idx + 1))
|
||||
if isinstance(key, int):
|
||||
return DateSingleItemBuilder(self._config._new(key, key + 1))
|
||||
start, stop = key.start, key.stop
|
||||
if isinstance(start, (date, datetime)):
|
||||
start = _date_to_index(self._config.index, start)
|
||||
if isinstance(stop, (date, datetime)):
|
||||
stop = _date_to_index(self._config.index, stop)
|
||||
return DateRangeBuilder(self._config._new(start, stop))
|
||||
|
||||
def head(self, n: int = 10) -> DateRangeBuilder[T]:
|
||||
"""Get the first n items."""
|
||||
return DateRangeBuilder(self._config._new(end=n))
|
||||
|
||||
def tail(self, n: int = 10) -> DateRangeBuilder[T]:
|
||||
"""Get the last n items."""
|
||||
return DateRangeBuilder(self._config._new(end=0) if n == 0 else self._config._new(start=-n))
|
||||
|
||||
def skip(self, n: int) -> DateSkippedBuilder[T]:
|
||||
"""Skip the first n items."""
|
||||
return DateSkippedBuilder(self._config._new(start=n))
|
||||
|
||||
def fetch(self) -> DateSeriesData[T]:
|
||||
"""Fetch all data."""
|
||||
return self._config.get_date_series()
|
||||
|
||||
def fetch_csv(self) -> str:
|
||||
"""Fetch all data as CSV."""
|
||||
return self._config.get_csv()
|
||||
|
||||
def len(self) -> int:
|
||||
"""Total number of data points for this series."""
|
||||
return self._config.get_len()
|
||||
|
||||
def version(self) -> Version:
|
||||
"""Current version of the series."""
|
||||
return self._config.get_version()
|
||||
|
||||
def path(self) -> str:
|
||||
"""Get the base endpoint path."""
|
||||
return self._config.path()
|
||||
|
||||
|
||||
class MetricPattern(Protocol[T]):
|
||||
"""Protocol for metric patterns with different index sets."""
|
||||
# Type aliases for non-generic usage
|
||||
AnySeriesEndpoint = SeriesEndpoint[Any]
|
||||
AnyDateSeriesEndpoint = DateSeriesEndpoint[Any]
|
||||
|
||||
|
||||
class SeriesPattern(Protocol[T]):
|
||||
"""Protocol for series patterns with different index sets."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Get the metric name."""
|
||||
"""Get the series name."""
|
||||
...
|
||||
|
||||
def indexes(self) -> List[str]:
|
||||
"""Get the list of available indexes for this metric."""
|
||||
"""Get the list of available indexes for this series."""
|
||||
...
|
||||
|
||||
def get(self, index: Index) -> Optional[MetricEndpointBuilder[T]]:
|
||||
def get(self, index: Index) -> Optional[SeriesEndpoint[T]]:
|
||||
"""Get an endpoint builder for a specific index, if supported."""
|
||||
...
|
||||
|
||||
@@ -361,7 +635,7 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
|
||||
if j > 0 {
|
||||
write!(output, ", ").unwrap();
|
||||
}
|
||||
write!(output, "'{}'", index.serialize_long()).unwrap();
|
||||
write!(output, "'{}'", index.name()).unwrap();
|
||||
}
|
||||
// Single-element tuple needs trailing comma
|
||||
if pattern.indexes.len() == 1 {
|
||||
@@ -371,11 +645,14 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
|
||||
}
|
||||
writeln!(output).unwrap();
|
||||
|
||||
// Generate helper function
|
||||
// Generate helper functions
|
||||
writeln!(
|
||||
output,
|
||||
r#"def _ep(c: BrkClientBase, n: str, i: Index) -> MetricEndpointBuilder:
|
||||
return MetricEndpointBuilder(c, n, i)
|
||||
r#"def _ep(c: BrkClient, n: str, i: Index) -> SeriesEndpoint[Any]:
|
||||
return SeriesEndpoint(c, n, i)
|
||||
|
||||
def _dep(c: BrkClient, n: str, i: Index) -> DateSeriesEndpoint[Any]:
|
||||
return DateSeriesEndpoint(c, n, i)
|
||||
"#
|
||||
)
|
||||
.unwrap();
|
||||
@@ -390,16 +667,21 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
|
||||
writeln!(output, "class {}(Generic[T]):", by_class_name).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" def __init__(self, c: BrkClientBase, n: str): self._c, self._n = c, n"
|
||||
" def __init__(self, c: BrkClient, n: str): self._c, self._n = c, n"
|
||||
)
|
||||
.unwrap();
|
||||
for index in &pattern.indexes {
|
||||
let method_name = index_to_field_name(index);
|
||||
let index_name = index.serialize_long();
|
||||
let index_name = index.name();
|
||||
let (builder_type, helper) = if index.is_date_based() {
|
||||
("DateSeriesEndpoint", "_dep")
|
||||
} else {
|
||||
("SeriesEndpoint", "_ep")
|
||||
};
|
||||
writeln!(
|
||||
output,
|
||||
" def {}(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, '{}')",
|
||||
method_name, index_name
|
||||
" def {}(self) -> {}[T]: return {}(self._c, self._n, '{}')",
|
||||
method_name, builder_type, helper, index_name
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -407,18 +689,24 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
|
||||
|
||||
// Generate the main accessor class
|
||||
writeln!(output, "class {}(Generic[T]):", pattern.name).unwrap();
|
||||
writeln!(output, " by: {}[T]", by_class_name).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" def __init__(self, c: BrkClientBase, n: str): self._n, self.by = n, {}(c, n)",
|
||||
" def __init__(self, c: BrkClient, n: str): self._n, self.by = n, {}(c, n)",
|
||||
by_class_name
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " @property").unwrap();
|
||||
writeln!(output, " def name(self) -> str: return self._n").unwrap();
|
||||
writeln!(output, " def indexes(self) -> List[str]: return list({})", idx_var).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" def get(self, index: Index) -> Optional[MetricEndpointBuilder[T]]: return _ep(self.by._c, self._n, index) if index in {} else None",
|
||||
" def indexes(self) -> List[str]: return list({})",
|
||||
idx_var
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" def get(self, index: Index) -> Optional[SeriesEndpoint[T]]: return _ep(self.by._c, self._n, index) if index in {} else None",
|
||||
idx_var
|
||||
)
|
||||
.unwrap();
|
||||
@@ -450,15 +738,30 @@ pub fn generate_structural_patterns(
|
||||
" \"\"\"Pattern struct for repeated tree structure.\"\"\""
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Skip constructor for non-parameterizable patterns (inlined at tree level)
|
||||
if !metadata.is_parameterizable(&pattern.name) {
|
||||
writeln!(output, " pass\n").unwrap();
|
||||
continue;
|
||||
}
|
||||
|
||||
writeln!(output, " ").unwrap();
|
||||
if pattern.is_templated() {
|
||||
writeln!(
|
||||
output,
|
||||
" def __init__(self, client: BrkClient, acc: str, disc: str):"
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
output,
|
||||
" def __init__(self, client: BrkClient, acc: str):"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
writeln!(
|
||||
output,
|
||||
" def __init__(self, client: BrkClientBase, acc: str):"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" \"\"\"Create pattern node with accumulated metric name.\"\"\""
|
||||
" \"\"\"Create pattern node with accumulated series name.\"\"\""
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@ pub mod client;
|
||||
pub mod tree;
|
||||
pub mod types;
|
||||
|
||||
use std::{fmt::Write, fs, io, path::Path};
|
||||
use std::{fmt::Write, io, path::Path};
|
||||
|
||||
use super::write_if_changed;
|
||||
use crate::{ClientMetadata, Endpoint, TypeSchemas};
|
||||
|
||||
/// Generate Python client from metadata and OpenAPI endpoints.
|
||||
@@ -24,14 +25,36 @@ pub fn generate_python_client(
|
||||
|
||||
writeln!(output, "# Auto-generated BRK Python client").unwrap();
|
||||
writeln!(output, "# Do not edit manually\n").unwrap();
|
||||
writeln!(output, "from __future__ import annotations").unwrap();
|
||||
writeln!(output, "from dataclasses import dataclass").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
"from typing import TypeVar, Generic, Any, Optional, List, Literal, TypedDict, Union, Protocol, overload"
|
||||
"from typing import TypeVar, Generic, Any, Dict, Optional, List, Iterator, Literal, TypedDict, Union, Protocol, overload, Tuple, TYPE_CHECKING"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
"from http.client import HTTPSConnection, HTTPConnection"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, "from http.client import HTTPSConnection, HTTPConnection").unwrap();
|
||||
writeln!(output, "from urllib.parse import urlparse").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
"from datetime import date, datetime, timedelta, timezone"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, "import json\n").unwrap();
|
||||
writeln!(output, "if TYPE_CHECKING:").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" import pandas as pd # type: ignore[import-not-found]"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" import polars as pl # type: ignore[import-not-found]\n"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, "T = TypeVar('T')\n").unwrap();
|
||||
|
||||
types::generate_type_definitions(&mut output, schemas);
|
||||
@@ -42,7 +65,7 @@ pub fn generate_python_client(
|
||||
tree::generate_tree_classes(&mut output, &metadata.catalog, metadata);
|
||||
api::generate_main_client(&mut output, endpoints);
|
||||
|
||||
fs::write(output_path, output)?;
|
||||
write_if_changed(output_path, &output)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
//! Python tree structure generation.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::collections::BTreeSet;
|
||||
use std::fmt::Write;
|
||||
|
||||
use brk_types::TreeNode;
|
||||
|
||||
use crate::{
|
||||
ClientMetadata, GenericSyntax, PatternField, PythonSyntax, build_child_path,
|
||||
generate_leaf_field, prepare_tree_node, to_snake_case,
|
||||
ClientMetadata, LanguageSyntax, PatternField, PythonSyntax, build_child_path,
|
||||
generate_leaf_field, generate_tree_node_field, prepare_tree_node,
|
||||
};
|
||||
|
||||
/// Generate tree classes
|
||||
pub fn generate_tree_classes(output: &mut String, catalog: &TreeNode, metadata: &ClientMetadata) {
|
||||
writeln!(output, "# Metrics tree classes\n").unwrap();
|
||||
writeln!(output, "# Series tree classes\n").unwrap();
|
||||
|
||||
let pattern_lookup = metadata.pattern_lookup();
|
||||
let mut generated = HashSet::new();
|
||||
let mut generated = BTreeSet::new();
|
||||
generate_tree_class(
|
||||
output,
|
||||
"MetricsTree",
|
||||
"SeriesTree",
|
||||
"",
|
||||
catalog,
|
||||
&pattern_lookup,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
&mut generated,
|
||||
);
|
||||
@@ -33,9 +33,9 @@ fn generate_tree_class(
|
||||
name: &str,
|
||||
path: &str,
|
||||
node: &TreeNode,
|
||||
pattern_lookup: &std::collections::HashMap<Vec<PatternField>, String>,
|
||||
pattern_lookup: &std::collections::BTreeMap<Vec<PatternField>, String>,
|
||||
metadata: &ClientMetadata,
|
||||
generated: &mut HashSet<String>,
|
||||
generated: &mut BTreeSet<String>,
|
||||
) {
|
||||
let Some(ctx) = prepare_tree_node(node, name, path, pattern_lookup, metadata, generated) else {
|
||||
return;
|
||||
@@ -60,45 +60,44 @@ fn generate_tree_class(
|
||||
|
||||
// THEN generate the current class (after all children are defined)
|
||||
writeln!(output, "class {}:", name).unwrap();
|
||||
writeln!(output, " \"\"\"Metrics tree node.\"\"\"").unwrap();
|
||||
writeln!(output, " \"\"\"Series tree node.\"\"\"").unwrap();
|
||||
writeln!(output, " ").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" def __init__(self, client: BrkClientBase, base_path: str = ''):"
|
||||
" def __init__(self, client: BrkClient, base_path: str = ''):"
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if ctx.children.is_empty() {
|
||||
writeln!(output, " pass").unwrap();
|
||||
}
|
||||
|
||||
let syntax = PythonSyntax;
|
||||
for child in &ctx.children {
|
||||
let field_name_py = to_snake_case(child.name);
|
||||
|
||||
if child.is_leaf {
|
||||
if let TreeNode::Leaf(leaf) = child.node {
|
||||
generate_leaf_field(output, &syntax, "client", child.name, leaf, metadata, " ");
|
||||
generate_leaf_field(
|
||||
output, &syntax, "client", child.name, leaf, metadata, " ",
|
||||
);
|
||||
}
|
||||
} else if child.should_inline {
|
||||
// Inline class
|
||||
let field_name = syntax.field_name(child.name);
|
||||
writeln!(
|
||||
output,
|
||||
" self.{}: {} = {}(client)",
|
||||
field_name_py, child.inline_type_name, child.inline_type_name
|
||||
field_name, child.inline_type_name, child.inline_type_name
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
// Use pattern class with metric base
|
||||
let py_type = metadata.resolve_tree_field_type(
|
||||
&child.field,
|
||||
child.child_fields.as_deref(),
|
||||
name,
|
||||
child.name,
|
||||
GenericSyntax::PYTHON,
|
||||
);
|
||||
writeln!(
|
||||
generate_tree_node_field(
|
||||
output,
|
||||
" self.{}: {} = {}(client, '{}')",
|
||||
field_name_py, py_type, child.field.rust_type, child.base_result.base
|
||||
)
|
||||
.unwrap();
|
||||
&syntax,
|
||||
&child.field,
|
||||
metadata,
|
||||
" ",
|
||||
"client",
|
||||
&child.base_result,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
//! Python type definitions generation.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt::Write;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{TypeSchemas, escape_python_keyword, generators::MANUAL_GENERIC_TYPES, get_union_variants, ref_to_type_name};
|
||||
use crate::{
|
||||
TypeSchemas, escape_python_keyword, generators::MANUAL_GENERIC_TYPES, get_union_variants,
|
||||
ref_to_type_name,
|
||||
};
|
||||
|
||||
/// Generate type definitions from schemas.
|
||||
pub fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
|
||||
@@ -32,7 +35,7 @@ pub fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
|
||||
|
||||
// Generate simple type aliases first
|
||||
// Quote references to TypedDicts since they're defined after
|
||||
let typed_dict_set: HashSet<_> = typed_dicts.iter().cloned().collect();
|
||||
let typed_dict_set: BTreeSet<_> = typed_dicts.iter().cloned().collect();
|
||||
for name in type_aliases {
|
||||
let schema = &schemas[&name];
|
||||
let type_desc = schema.get("description").and_then(|d| d.as_str());
|
||||
@@ -49,7 +52,10 @@ pub fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
|
||||
for name in typed_dicts {
|
||||
let schema = &schemas[&name];
|
||||
let type_desc = schema.get("description").and_then(|d| d.as_str());
|
||||
let props = schema.get("properties").and_then(|p| p.as_object()).unwrap();
|
||||
let props = schema
|
||||
.get("properties")
|
||||
.and_then(|p| p.as_object())
|
||||
.unwrap();
|
||||
|
||||
writeln!(output, "class {}(TypedDict):", name).unwrap();
|
||||
|
||||
@@ -100,17 +106,18 @@ pub fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
|
||||
/// Types that reference other types (via $ref) must be defined after their dependencies.
|
||||
fn topological_sort_schemas(schemas: &TypeSchemas) -> Vec<String> {
|
||||
// Build dependency graph
|
||||
let mut deps: HashMap<String, HashSet<String>> = HashMap::new();
|
||||
let mut deps: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
|
||||
for (name, schema) in schemas {
|
||||
let mut type_deps = HashSet::new();
|
||||
let mut type_deps = BTreeSet::new();
|
||||
collect_schema_refs(schema, &mut type_deps);
|
||||
// Only keep deps that are in our schemas
|
||||
type_deps.retain(|d| schemas.contains_key(d));
|
||||
// Only keep deps that are in our schemas, and drop self-references
|
||||
// (handled at emit time by quoting via current_type)
|
||||
type_deps.retain(|d| schemas.contains_key(d) && d != name);
|
||||
deps.insert(name.clone(), type_deps);
|
||||
}
|
||||
|
||||
// Kahn's algorithm for topological sort
|
||||
let mut in_degree: HashMap<String, usize> = HashMap::new();
|
||||
let mut in_degree: BTreeMap<String, usize> = BTreeMap::new();
|
||||
for name in schemas.keys() {
|
||||
in_degree.insert(name.clone(), 0);
|
||||
}
|
||||
@@ -148,7 +155,7 @@ fn topological_sort_schemas(schemas: &TypeSchemas) -> Vec<String> {
|
||||
result.reverse();
|
||||
|
||||
// Add any types that weren't processed (e.g., due to circular refs or other edge cases)
|
||||
let result_set: HashSet<_> = result.iter().cloned().collect();
|
||||
let result_set: BTreeSet<_> = result.iter().cloned().collect();
|
||||
let mut missing: Vec<_> = schemas
|
||||
.keys()
|
||||
.filter(|k| !result_set.contains(*k))
|
||||
@@ -161,7 +168,7 @@ fn topological_sort_schemas(schemas: &TypeSchemas) -> Vec<String> {
|
||||
}
|
||||
|
||||
/// Collect all type references ($ref) from a schema
|
||||
fn collect_schema_refs(schema: &Value, refs: &mut HashSet<String>) {
|
||||
fn collect_schema_refs(schema: &Value, refs: &mut BTreeSet<String>) {
|
||||
match schema {
|
||||
Value::Object(map) => {
|
||||
if let Some(ref_path) = map.get("$ref").and_then(|r| r.as_str())
|
||||
@@ -215,7 +222,7 @@ fn json_type_to_python(ty: &str, schema: &Value, current_type: Option<&str>) ->
|
||||
pub fn schema_to_python_type(
|
||||
schema: &Value,
|
||||
current_type: Option<&str>,
|
||||
quote_types: Option<&HashSet<String>>,
|
||||
quote_types: Option<&BTreeSet<String>>,
|
||||
) -> String {
|
||||
if let Some(all_of) = schema.get("allOf").and_then(|v| v.as_array()) {
|
||||
for item in all_of {
|
||||
@@ -230,8 +237,8 @@ pub fn schema_to_python_type(
|
||||
if let Some(ref_path) = schema.get("$ref").and_then(|r| r.as_str()) {
|
||||
let type_name = ref_to_type_name(ref_path).unwrap_or("Any");
|
||||
// Quote self-references or types in quote_types set
|
||||
let should_quote = current_type == Some(type_name)
|
||||
|| quote_types.is_some_and(|qt| qt.contains(type_name));
|
||||
let should_quote =
|
||||
current_type == Some(type_name) || quote_types.is_some_and(|qt| qt.contains(type_name));
|
||||
if should_quote {
|
||||
return format!("\"{}\"", type_name);
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ use super::types::js_type_to_rust;
|
||||
pub fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) {
|
||||
writeln!(
|
||||
output,
|
||||
r#"/// Main BRK client with metrics tree and API methods.
|
||||
r#"/// Main BRK client with series tree and API methods.
|
||||
pub struct BrkClient {{
|
||||
base: Arc<BrkClientBase>,
|
||||
metrics: MetricsTree,
|
||||
series: SeriesTree,
|
||||
}}
|
||||
|
||||
impl BrkClient {{
|
||||
@@ -23,40 +23,54 @@ impl BrkClient {{
|
||||
/// Create a new client with the given base URL.
|
||||
pub fn new(base_url: impl Into<String>) -> Self {{
|
||||
let base = Arc::new(BrkClientBase::new(base_url));
|
||||
let metrics = MetricsTree::new(base.clone(), String::new());
|
||||
Self {{ base, metrics }}
|
||||
let series = SeriesTree::new(base.clone(), String::new());
|
||||
Self {{ base, series }}
|
||||
}}
|
||||
|
||||
/// Create a new client with options.
|
||||
pub fn with_options(options: BrkClientOptions) -> Self {{
|
||||
let base = Arc::new(BrkClientBase::with_options(options));
|
||||
let metrics = MetricsTree::new(base.clone(), String::new());
|
||||
Self {{ base, metrics }}
|
||||
let series = SeriesTree::new(base.clone(), String::new());
|
||||
Self {{ base, series }}
|
||||
}}
|
||||
|
||||
/// Get the metrics tree for navigating metrics.
|
||||
pub fn metrics(&self) -> &MetricsTree {{
|
||||
&self.metrics
|
||||
/// Get the series tree for navigating series.
|
||||
pub fn series(&self) -> &SeriesTree {{
|
||||
&self.series
|
||||
}}
|
||||
|
||||
/// Create a dynamic metric endpoint builder for any metric/index combination.
|
||||
/// Create a dynamic series endpoint builder for any series/index combination.
|
||||
///
|
||||
/// Use this for programmatic access when the metric name is determined at runtime.
|
||||
/// For type-safe access, use the `metrics()` tree instead.
|
||||
/// Use this for programmatic access when the series name is determined at runtime.
|
||||
/// For type-safe access, use the `series()` tree instead.
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let data = client.metric("realized_price", Index::Height)
|
||||
/// let data = client.series("realized_price", Index::Height)
|
||||
/// .last(10)
|
||||
/// .json::<f64>()?;
|
||||
/// ```
|
||||
pub fn metric(&self, metric: impl Into<Metric>, index: Index) -> MetricEndpointBuilder<serde_json::Value> {{
|
||||
MetricEndpointBuilder::new(
|
||||
pub fn series_endpoint(&self, series: impl Into<SeriesName>, index: Index) -> SeriesEndpoint<serde_json::Value> {{
|
||||
SeriesEndpoint::new(
|
||||
self.base.clone(),
|
||||
Arc::from(metric.into().as_str()),
|
||||
Arc::from(series.into().as_str()),
|
||||
index,
|
||||
)
|
||||
}}
|
||||
|
||||
/// Create a dynamic date-based series endpoint builder.
|
||||
///
|
||||
/// Returns `Err` if the index is not date-based.
|
||||
pub fn date_series_endpoint(&self, series: impl Into<SeriesName>, index: Index) -> Result<DateSeriesEndpoint<serde_json::Value>> {{
|
||||
if !index.is_date_based() {{
|
||||
return Err(BrkError {{ message: format!("{{}} is not a date-based index", index.name()) }});
|
||||
}}
|
||||
Ok(DateSeriesEndpoint::new(
|
||||
self.base.clone(),
|
||||
Arc::from(series.into().as_str()),
|
||||
index,
|
||||
))
|
||||
}}
|
||||
"#,
|
||||
VERSION = VERSION
|
||||
)
|
||||
@@ -73,85 +87,200 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
if !endpoint.should_generate() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let method_name = endpoint_to_method_name(endpoint);
|
||||
let base_return_type = endpoint
|
||||
.response_type
|
||||
.as_deref()
|
||||
.map(js_type_to_rust)
|
||||
.unwrap_or_else(|| "serde_json::Value".to_string());
|
||||
|
||||
let return_type = if endpoint.supports_csv {
|
||||
format!("FormatResponse<{}>", base_return_type)
|
||||
} else {
|
||||
base_return_type.clone()
|
||||
};
|
||||
|
||||
writeln!(
|
||||
output,
|
||||
" /// {}",
|
||||
endpoint.summary.as_deref().unwrap_or(&method_name)
|
||||
)
|
||||
.unwrap();
|
||||
if let Some(desc) = &endpoint.description
|
||||
&& endpoint.summary.as_ref() != Some(desc)
|
||||
{
|
||||
writeln!(output, " ///").unwrap();
|
||||
write_description(output, desc, " /// ", " ///");
|
||||
match endpoint.method.as_str() {
|
||||
"GET" => generate_get_method(output, endpoint),
|
||||
"POST" => generate_post_method(output, endpoint),
|
||||
_ => continue,
|
||||
}
|
||||
// Add endpoint path
|
||||
writeln!(output, " ///").unwrap();
|
||||
writeln!(output, " /// Endpoint: `{} {}`", endpoint.method.to_uppercase(), endpoint.path).unwrap();
|
||||
|
||||
let params = build_method_params(endpoint);
|
||||
writeln!(
|
||||
output,
|
||||
" pub fn {}(&self{}) -> Result<{}> {{",
|
||||
method_name, params, return_type
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (path, index_arg) = build_path_template(endpoint);
|
||||
|
||||
if endpoint.query_params.is_empty() {
|
||||
writeln!(output, " self.base.get_json(&format!(\"{}\"{}))", path, index_arg).unwrap();
|
||||
} else {
|
||||
writeln!(output, " let mut query = Vec::new();").unwrap();
|
||||
for param in &endpoint.query_params {
|
||||
if param.required {
|
||||
writeln!(
|
||||
output,
|
||||
" query.push(format!(\"{}={{}}\", {}));",
|
||||
param.name, param.name
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
output,
|
||||
" if let Some(v) = {} {{ query.push(format!(\"{}={{}}\", v)); }}",
|
||||
param.name, param.name
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
writeln!(output, " let query_str = if query.is_empty() {{ String::new() }} else {{ format!(\"?{{}}\", query.join(\"&\")) }};").unwrap();
|
||||
writeln!(output, " let path = format!(\"{}{{}}\"{}, query_str);", path, index_arg).unwrap();
|
||||
|
||||
if endpoint.supports_csv {
|
||||
writeln!(output, " if format == Some(Format::CSV) {{").unwrap();
|
||||
writeln!(output, " self.base.get_text(&path).map(FormatResponse::Csv)").unwrap();
|
||||
writeln!(output, " }} else {{").unwrap();
|
||||
writeln!(output, " self.base.get_json(&path).map(FormatResponse::Json)").unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
} else {
|
||||
writeln!(output, " self.base.get_json(&path)").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(output, " }}\n").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_get_method(output: &mut String, endpoint: &Endpoint) {
|
||||
let method_name = endpoint_to_method_name(endpoint);
|
||||
let return_type = build_return_type(endpoint);
|
||||
|
||||
write_method_doc(output, endpoint);
|
||||
|
||||
let params = build_method_params(endpoint);
|
||||
writeln!(
|
||||
output,
|
||||
" pub fn {}(&self{}) -> Result<{}> {{",
|
||||
method_name, params, return_type
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (path, index_arg) = build_path_template(endpoint);
|
||||
let fetch_method = if endpoint.returns_binary() {
|
||||
"get_bytes"
|
||||
} else if endpoint.returns_json() {
|
||||
"get_json"
|
||||
} else {
|
||||
"get_text"
|
||||
};
|
||||
|
||||
if endpoint.query_params.is_empty() {
|
||||
writeln!(
|
||||
output,
|
||||
" self.base.{}(&format!(\"{}\"{}))",
|
||||
fetch_method, path, index_arg
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
write_query_assembly(output, endpoint, &path, index_arg);
|
||||
|
||||
if endpoint.supports_csv {
|
||||
writeln!(output, " if format == Some(Format::CSV) {{").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" self.base.get_text(&path).map(FormatResponse::Csv)"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " }} else {{").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" self.base.{}(&path).map(FormatResponse::Json)",
|
||||
fetch_method
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
} else {
|
||||
writeln!(output, " self.base.{}(&path)", fetch_method).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(output, " }}\n").unwrap();
|
||||
}
|
||||
|
||||
fn generate_post_method(output: &mut String, endpoint: &Endpoint) {
|
||||
let method_name = endpoint_to_method_name(endpoint);
|
||||
let return_type = build_return_type(endpoint);
|
||||
|
||||
write_method_doc(output, endpoint);
|
||||
|
||||
let mut params = build_method_params(endpoint);
|
||||
if endpoint.request_body.is_some() {
|
||||
params.push_str(", body: &str");
|
||||
}
|
||||
writeln!(
|
||||
output,
|
||||
" pub fn {}(&self{}) -> Result<{}> {{",
|
||||
method_name, params, return_type
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (path, index_arg) = build_path_template(endpoint);
|
||||
let body_arg = if endpoint.request_body.is_some() {
|
||||
"body"
|
||||
} else {
|
||||
"\"\""
|
||||
};
|
||||
let fetch_method = if endpoint.returns_binary() {
|
||||
"post_bytes"
|
||||
} else if endpoint.returns_json() {
|
||||
"post_json"
|
||||
} else {
|
||||
"post_text"
|
||||
};
|
||||
|
||||
if endpoint.query_params.is_empty() {
|
||||
writeln!(
|
||||
output,
|
||||
" self.base.{}(&format!(\"{}\"{}), {})",
|
||||
fetch_method, path, index_arg, body_arg
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
write_query_assembly(output, endpoint, &path, index_arg);
|
||||
writeln!(
|
||||
output,
|
||||
" self.base.{}(&path, {})",
|
||||
fetch_method, body_arg
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(output, " }}\n").unwrap();
|
||||
}
|
||||
|
||||
fn build_return_type(endpoint: &Endpoint) -> String {
|
||||
let base = if endpoint.returns_binary() {
|
||||
"Vec<u8>".to_string()
|
||||
} else if endpoint.returns_text() {
|
||||
"String".to_string()
|
||||
} else {
|
||||
endpoint
|
||||
.schema_name()
|
||||
.map(js_type_to_rust)
|
||||
.unwrap_or_else(|| "String".to_string())
|
||||
};
|
||||
if endpoint.supports_csv {
|
||||
format!("FormatResponse<{}>", base)
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
|
||||
fn write_method_doc(output: &mut String, endpoint: &Endpoint) {
|
||||
let method_name = endpoint_to_method_name(endpoint);
|
||||
writeln!(
|
||||
output,
|
||||
" /// {}",
|
||||
endpoint.summary.as_deref().unwrap_or(&method_name)
|
||||
)
|
||||
.unwrap();
|
||||
if let Some(desc) = &endpoint.description
|
||||
&& endpoint.summary.as_ref() != Some(desc)
|
||||
{
|
||||
writeln!(output, " ///").unwrap();
|
||||
write_description(output, desc, " /// ", " ///");
|
||||
}
|
||||
writeln!(output, " ///").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" /// Endpoint: `{} {}`",
|
||||
endpoint.method.to_uppercase(),
|
||||
endpoint.path
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn write_query_assembly(output: &mut String, endpoint: &Endpoint, path: &str, index_arg: &str) {
|
||||
writeln!(output, " let mut query = Vec::new();").unwrap();
|
||||
for param in &endpoint.query_params {
|
||||
let ident = sanitize_ident(¶m.name);
|
||||
let is_array = param.param_type.ends_with("[]");
|
||||
if is_array {
|
||||
writeln!(
|
||||
output,
|
||||
" for v in {} {{ query.push(format!(\"{}={{}}\", v)); }}",
|
||||
ident, param.name
|
||||
)
|
||||
.unwrap();
|
||||
} else if param.required {
|
||||
writeln!(
|
||||
output,
|
||||
" query.push(format!(\"{}={{}}\", {}));",
|
||||
param.name, ident
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
output,
|
||||
" if let Some(v) = {} {{ query.push(format!(\"{}={{}}\", v)); }}",
|
||||
ident, param.name
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
writeln!(output, " let query_str = if query.is_empty() {{ String::new() }} else {{ format!(\"?{{}}\", query.join(\"&\")) }};").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" let path = format!(\"{}{{}}\"{}, query_str);",
|
||||
path, index_arg
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn endpoint_to_method_name(endpoint: &Endpoint) -> String {
|
||||
to_snake_case(&endpoint.operation_name())
|
||||
}
|
||||
@@ -160,34 +289,46 @@ fn build_method_params(endpoint: &Endpoint) -> String {
|
||||
let mut params = Vec::new();
|
||||
for param in &endpoint.path_params {
|
||||
let rust_type = param_type_to_rust(¶m.param_type);
|
||||
params.push(format!(", {}: {}", param.name, rust_type));
|
||||
params.push(format!(", {}: {}", sanitize_ident(¶m.name), rust_type));
|
||||
}
|
||||
for param in &endpoint.query_params {
|
||||
let rust_type = param_type_to_rust(¶m.param_type);
|
||||
let name = sanitize_ident(¶m.name);
|
||||
if param.required {
|
||||
params.push(format!(", {}: {}", param.name, rust_type));
|
||||
params.push(format!(", {}: {}", name, rust_type));
|
||||
} else {
|
||||
params.push(format!(", {}: Option<{}>", param.name, rust_type));
|
||||
params.push(format!(", {}: Option<{}>", name, rust_type));
|
||||
}
|
||||
}
|
||||
params.join("")
|
||||
}
|
||||
|
||||
/// Strip characters invalid in Rust identifiers (e.g. `[]` from `txId[]`).
|
||||
fn sanitize_ident(name: &str) -> String {
|
||||
name.replace(['[', ']'], "")
|
||||
}
|
||||
|
||||
/// Convert parameter type to Rust type for function signatures.
|
||||
fn param_type_to_rust(param_type: &str) -> String {
|
||||
if let Some(inner) = param_type.strip_suffix("[]") {
|
||||
return format!("&[{}]", param_type_to_rust(inner));
|
||||
}
|
||||
match param_type {
|
||||
"string" | "*" => "&str".to_string(),
|
||||
"integer" | "number" => "i64".to_string(),
|
||||
"boolean" => "bool".to_string(),
|
||||
other => other.to_string(), // Domain types like Index, Metric, Format
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build path template and extra format args for Index params.
|
||||
fn build_path_template(endpoint: &Endpoint) -> (String, &'static str) {
|
||||
let has_index_param = endpoint.path_params.iter().any(|p| p.name == "index" && p.param_type == "Index");
|
||||
let has_index_param = endpoint
|
||||
.path_params
|
||||
.iter()
|
||||
.any(|p| p.name == "index" && p.param_type == "Index");
|
||||
if has_index_param {
|
||||
(endpoint.path.replace("{index}", "{}"), ", index.serialize_long()")
|
||||
(endpoint.path.replace("{index}", "{}"), ", index.name()")
|
||||
} else {
|
||||
(endpoint.path.clone(), "")
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::fmt::Write;
|
||||
|
||||
use crate::{
|
||||
ClientMetadata, GenericSyntax, IndexSetPattern, RustSyntax, StructuralPattern,
|
||||
generate_parameterized_field, index_to_field_name, to_snake_case,
|
||||
escape_rust_keyword, generate_parameterized_field, index_to_field_name, to_snake_case,
|
||||
};
|
||||
|
||||
/// Generate import statements.
|
||||
@@ -59,64 +59,85 @@ impl Default for BrkClientOptions {{
|
||||
}}
|
||||
}}
|
||||
|
||||
/// Base HTTP client for making requests.
|
||||
/// Base HTTP client for making requests. Reuses connections via ureq::Agent.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BrkClientBase {{
|
||||
agent: ureq::Agent,
|
||||
base_url: String,
|
||||
timeout_secs: u64,
|
||||
}}
|
||||
|
||||
impl BrkClientBase {{
|
||||
/// Create a new client with the given base URL.
|
||||
pub fn new(base_url: impl Into<String>) -> Self {{
|
||||
Self {{
|
||||
base_url: base_url.into(),
|
||||
timeout_secs: 30,
|
||||
}}
|
||||
Self::with_options(BrkClientOptions {{ base_url: base_url.into(), ..Default::default() }})
|
||||
}}
|
||||
|
||||
/// Create a new client with options.
|
||||
pub fn with_options(options: BrkClientOptions) -> Self {{
|
||||
let agent = ureq::Agent::config_builder()
|
||||
.timeout_global(Some(std::time::Duration::from_secs(options.timeout_secs)))
|
||||
.build()
|
||||
.into();
|
||||
Self {{
|
||||
base_url: options.base_url,
|
||||
timeout_secs: options.timeout_secs,
|
||||
agent,
|
||||
base_url: options.base_url.trim_end_matches('/').to_string(),
|
||||
}}
|
||||
}}
|
||||
|
||||
fn get(&self, path: &str) -> Result<minreq::Response> {{
|
||||
let base = self.base_url.trim_end_matches('/');
|
||||
let url = format!("{{}}{{}}", base, path);
|
||||
let response = minreq::get(&url)
|
||||
.with_timeout(self.timeout_secs)
|
||||
.send()
|
||||
.map_err(|e| BrkError {{ message: e.to_string() }})?;
|
||||
|
||||
if response.status_code >= 400 {{
|
||||
return Err(BrkError {{
|
||||
message: format!("HTTP {{}}", response.status_code),
|
||||
}});
|
||||
}}
|
||||
|
||||
Ok(response)
|
||||
fn url(&self, path: &str) -> String {{
|
||||
format!("{{}}{{}}", self.base_url, path)
|
||||
}}
|
||||
|
||||
/// Make a GET request and deserialize JSON response.
|
||||
pub fn get_json<T: DeserializeOwned>(&self, path: &str) -> Result<T> {{
|
||||
self.get(path)?
|
||||
.json()
|
||||
self.agent.get(&self.url(path))
|
||||
.call()
|
||||
.and_then(|mut r| r.body_mut().read_json())
|
||||
.map_err(|e| BrkError {{ message: e.to_string() }})
|
||||
}}
|
||||
|
||||
/// Make a GET request and return raw text response.
|
||||
pub fn get_text(&self, path: &str) -> Result<String> {{
|
||||
self.get(path)?
|
||||
.as_str()
|
||||
.map(|s| s.to_string())
|
||||
self.agent.get(&self.url(path))
|
||||
.call()
|
||||
.and_then(|mut r| r.body_mut().read_to_string())
|
||||
.map_err(|e| BrkError {{ message: e.to_string() }})
|
||||
}}
|
||||
|
||||
/// Make a GET request and return raw bytes response.
|
||||
pub fn get_bytes(&self, path: &str) -> Result<Vec<u8>> {{
|
||||
self.agent.get(&self.url(path))
|
||||
.call()
|
||||
.and_then(|mut r| r.body_mut().read_to_vec())
|
||||
.map_err(|e| BrkError {{ message: e.to_string() }})
|
||||
}}
|
||||
|
||||
/// Make a POST request and deserialize JSON response.
|
||||
pub fn post_json<T: DeserializeOwned>(&self, path: &str, body: &str) -> Result<T> {{
|
||||
self.agent.post(&self.url(path))
|
||||
.send(body)
|
||||
.and_then(|mut r| r.body_mut().read_json())
|
||||
.map_err(|e| BrkError {{ message: e.to_string() }})
|
||||
}}
|
||||
|
||||
/// Make a POST request and return raw text response.
|
||||
pub fn post_text(&self, path: &str, body: &str) -> Result<String> {{
|
||||
self.agent.post(&self.url(path))
|
||||
.send(body)
|
||||
.and_then(|mut r| r.body_mut().read_to_string())
|
||||
.map_err(|e| BrkError {{ message: e.to_string() }})
|
||||
}}
|
||||
|
||||
/// Make a POST request and return raw bytes response.
|
||||
pub fn post_bytes(&self, path: &str, body: &str) -> Result<Vec<u8>> {{
|
||||
self.agent.post(&self.url(path))
|
||||
.send(body)
|
||||
.and_then(|mut r| r.body_mut().read_to_vec())
|
||||
.map_err(|e| BrkError {{ message: e.to_string() }})
|
||||
}}
|
||||
}}
|
||||
|
||||
/// Build metric name with suffix.
|
||||
/// Build series name with suffix.
|
||||
#[inline]
|
||||
fn _m(acc: &str, s: &str) -> String {{
|
||||
if s.is_empty() {{ acc.to_string() }}
|
||||
@@ -124,7 +145,7 @@ fn _m(acc: &str, s: &str) -> String {{
|
||||
else {{ format!("{{acc}}_{{s}}") }}
|
||||
}}
|
||||
|
||||
/// Build metric name with prefix.
|
||||
/// Build series name with prefix.
|
||||
#[inline]
|
||||
fn _p(prefix: &str, acc: &str) -> String {{
|
||||
if acc.is_empty() {{ prefix.to_string() }} else {{ format!("{{prefix}}_{{acc}}") }}
|
||||
@@ -135,23 +156,23 @@ fn _p(prefix: &str, acc: &str) -> String {{
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Generate the MetricPattern trait.
|
||||
pub fn generate_metric_pattern_trait(output: &mut String) {
|
||||
/// Generate the SeriesPattern trait.
|
||||
pub fn generate_series_pattern_trait(output: &mut String) {
|
||||
writeln!(
|
||||
output,
|
||||
r#"/// Non-generic trait for metric patterns (usable in collections).
|
||||
pub trait AnyMetricPattern {{
|
||||
/// Get the metric name.
|
||||
r#"/// Non-generic trait for series patterns (usable in collections).
|
||||
pub trait AnySeriesPattern {{
|
||||
/// Get the series name.
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Get the list of available indexes for this metric.
|
||||
/// Get the list of available indexes for this series.
|
||||
fn indexes(&self) -> &'static [Index];
|
||||
}}
|
||||
|
||||
/// Generic trait for metric patterns with endpoint access.
|
||||
pub trait MetricPattern<T>: AnyMetricPattern {{
|
||||
/// Generic trait for series patterns with endpoint access.
|
||||
pub trait SeriesPattern<T>: AnySeriesPattern {{
|
||||
/// Get an endpoint builder for a specific index, if supported.
|
||||
fn get(&self, index: Index) -> Option<MetricEndpointBuilder<T>>;
|
||||
fn get(&self, index: Index) -> Option<SeriesEndpoint<T>>;
|
||||
}}
|
||||
|
||||
"#
|
||||
@@ -159,7 +180,7 @@ pub trait MetricPattern<T>: AnyMetricPattern {{
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Generate the MetricEndpointBuilder structs with typestate pattern.
|
||||
/// Generate the SeriesEndpoint structs with typestate pattern.
|
||||
pub fn generate_endpoint(output: &mut String) {
|
||||
writeln!(
|
||||
output,
|
||||
@@ -179,7 +200,7 @@ impl EndpointConfig {{
|
||||
}}
|
||||
|
||||
fn path(&self) -> String {{
|
||||
format!("/api/metric/{{}}/{{}}", self.name, self.index.serialize_long())
|
||||
format!("/api/series/{{}}/{{}}", self.name, self.index.name())
|
||||
}}
|
||||
|
||||
fn build_path(&self, format: Option<&str>) -> String {{
|
||||
@@ -198,47 +219,49 @@ impl EndpointConfig {{
|
||||
fn get_text(&self, format: Option<&str>) -> Result<String> {{
|
||||
self.client.get_text(&self.build_path(format))
|
||||
}}
|
||||
|
||||
fn get_len(&self) -> Result<i64> {{
|
||||
self.client.get_json(&format!("/api/series/{{}}/{{}}/len", self.name, self.index.name()))
|
||||
}}
|
||||
|
||||
fn get_version(&self) -> Result<Version> {{
|
||||
self.client.get_json(&format!("/api/series/{{}}/{{}}/version", self.name, self.index.name()))
|
||||
}}
|
||||
}}
|
||||
|
||||
/// Initial builder for metric endpoint queries.
|
||||
/// Builder for series endpoint queries.
|
||||
///
|
||||
/// Use method chaining to specify the data range, then call `fetch()` or `fetch_csv()` to execute.
|
||||
/// Parameterized by element type `T` and response type `D` (defaults to `SeriesData<T>`).
|
||||
/// For date-based indexes, use `DateSeriesEndpoint<T>` which sets `D = DateSeriesData<T>`.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// // Fetch all data
|
||||
/// let data = endpoint.fetch()?;
|
||||
///
|
||||
/// // Get single item at index 5
|
||||
/// let data = endpoint.get(5).fetch()?;
|
||||
///
|
||||
/// // Get first 10 using range
|
||||
/// let data = endpoint.range(..10).fetch()?;
|
||||
///
|
||||
/// // Get range [100, 200)
|
||||
/// let data = endpoint.range(100..200).fetch()?;
|
||||
///
|
||||
/// // Get first 10 (convenience)
|
||||
/// let data = endpoint.take(10).fetch()?;
|
||||
///
|
||||
/// // Get last 10
|
||||
/// let data = endpoint.last(10).fetch()?;
|
||||
///
|
||||
/// // Iterator-style chaining
|
||||
/// let data = endpoint.skip(100).take(10).fetch()?;
|
||||
/// let data = endpoint.fetch()?; // all data
|
||||
/// let data = endpoint.get(5).fetch()?; // single item
|
||||
/// let data = endpoint.range(..10).fetch()?; // first 10
|
||||
/// let data = endpoint.range(100..200).fetch()?; // range [100, 200)
|
||||
/// let data = endpoint.take(10).fetch()?; // first 10 (convenience)
|
||||
/// let data = endpoint.last(10).fetch()?; // last 10
|
||||
/// let data = endpoint.skip(100).take(10).fetch()?; // iterator-style
|
||||
/// ```
|
||||
pub struct MetricEndpointBuilder<T> {{
|
||||
pub struct SeriesEndpoint<T, D = SeriesData<T>> {{
|
||||
config: EndpointConfig,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
_marker: std::marker::PhantomData<fn() -> (T, D)>,
|
||||
}}
|
||||
|
||||
impl<T: DeserializeOwned> MetricEndpointBuilder<T> {{
|
||||
/// Builder for date-based series endpoint queries.
|
||||
///
|
||||
/// Like `SeriesEndpoint` but returns `DateSeriesData` and provides
|
||||
/// date-based access methods (`get_date`, `date_range`).
|
||||
pub type DateSeriesEndpoint<T> = SeriesEndpoint<T, DateSeriesData<T>>;
|
||||
|
||||
impl<T: DeserializeOwned, D: DeserializeOwned> SeriesEndpoint<T, D> {{
|
||||
pub fn new(client: Arc<BrkClientBase>, name: Arc<str>, index: Index) -> Self {{
|
||||
Self {{ config: EndpointConfig::new(client, name, index), _marker: std::marker::PhantomData }}
|
||||
}}
|
||||
|
||||
/// Select a specific index position.
|
||||
pub fn get(mut self, index: usize) -> SingleItemBuilder<T> {{
|
||||
pub fn get(mut self, index: usize) -> SingleItemBuilder<T, D> {{
|
||||
self.config.start = Some(index as i64);
|
||||
self.config.end = Some(index as i64 + 1);
|
||||
SingleItemBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||
@@ -252,7 +275,7 @@ impl<T: DeserializeOwned> MetricEndpointBuilder<T> {{
|
||||
/// endpoint.range(100..110) // indices 100-109
|
||||
/// endpoint.range(100..) // from 100 to end
|
||||
/// ```
|
||||
pub fn range<R: RangeBounds<usize>>(mut self, range: R) -> RangeBuilder<T> {{
|
||||
pub fn range<R: RangeBounds<usize>>(mut self, range: R) -> RangeBuilder<T, D> {{
|
||||
self.config.start = match range.start_bound() {{
|
||||
Bound::Included(&n) => Some(n as i64),
|
||||
Bound::Excluded(&n) => Some(n as i64 + 1),
|
||||
@@ -267,12 +290,12 @@ impl<T: DeserializeOwned> MetricEndpointBuilder<T> {{
|
||||
}}
|
||||
|
||||
/// Take the first n items.
|
||||
pub fn take(self, n: usize) -> RangeBuilder<T> {{
|
||||
pub fn take(self, n: usize) -> RangeBuilder<T, D> {{
|
||||
self.range(..n)
|
||||
}}
|
||||
|
||||
/// Take the last n items.
|
||||
pub fn last(mut self, n: usize) -> RangeBuilder<T> {{
|
||||
pub fn last(mut self, n: usize) -> RangeBuilder<T, D> {{
|
||||
if n == 0 {{
|
||||
self.config.end = Some(0);
|
||||
}} else {{
|
||||
@@ -282,13 +305,13 @@ impl<T: DeserializeOwned> MetricEndpointBuilder<T> {{
|
||||
}}
|
||||
|
||||
/// Skip the first n items. Chain with `take(n)` to get a range.
|
||||
pub fn skip(mut self, n: usize) -> SkippedBuilder<T> {{
|
||||
pub fn skip(mut self, n: usize) -> SkippedBuilder<T, D> {{
|
||||
self.config.start = Some(n as i64);
|
||||
SkippedBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||
}}
|
||||
|
||||
/// Fetch all data as parsed JSON.
|
||||
pub fn fetch(self) -> Result<MetricData<T>> {{
|
||||
pub fn fetch(self) -> Result<D> {{
|
||||
self.config.get_json(None)
|
||||
}}
|
||||
|
||||
@@ -297,21 +320,64 @@ impl<T: DeserializeOwned> MetricEndpointBuilder<T> {{
|
||||
self.config.get_text(Some("csv"))
|
||||
}}
|
||||
|
||||
/// Total number of data points for this series.
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> Result<i64> {{
|
||||
self.config.get_len()
|
||||
}}
|
||||
|
||||
/// Current version of the series.
|
||||
pub fn version(&self) -> Result<Version> {{
|
||||
self.config.get_version()
|
||||
}}
|
||||
|
||||
/// Get the base endpoint path.
|
||||
pub fn path(&self) -> String {{
|
||||
self.config.path()
|
||||
}}
|
||||
}}
|
||||
|
||||
/// Builder for single item access.
|
||||
pub struct SingleItemBuilder<T> {{
|
||||
config: EndpointConfig,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
/// Date-specific methods available only on `DateSeriesEndpoint`.
|
||||
impl<T: DeserializeOwned> SeriesEndpoint<T, DateSeriesData<T>> {{
|
||||
/// Select a specific date position (for day-precision or coarser indexes).
|
||||
pub fn get_date(self, date: Date) -> SingleItemBuilder<T, DateSeriesData<T>> {{
|
||||
let index = self.config.index.date_to_index(date).unwrap_or(0);
|
||||
self.get(index)
|
||||
}}
|
||||
|
||||
/// Select a date range (for day-precision or coarser indexes).
|
||||
pub fn date_range(self, start: Date, end: Date) -> RangeBuilder<T, DateSeriesData<T>> {{
|
||||
let s = self.config.index.date_to_index(start).unwrap_or(0);
|
||||
let e = self.config.index.date_to_index(end).unwrap_or(0);
|
||||
self.range(s..e)
|
||||
}}
|
||||
|
||||
/// Select a specific timestamp position (works for all date-based indexes including sub-daily).
|
||||
pub fn get_timestamp(self, ts: Timestamp) -> SingleItemBuilder<T, DateSeriesData<T>> {{
|
||||
let index = self.config.index.timestamp_to_index(ts).unwrap_or(0);
|
||||
self.get(index)
|
||||
}}
|
||||
|
||||
/// Select a timestamp range (works for all date-based indexes including sub-daily).
|
||||
pub fn timestamp_range(self, start: Timestamp, end: Timestamp) -> RangeBuilder<T, DateSeriesData<T>> {{
|
||||
let s = self.config.index.timestamp_to_index(start).unwrap_or(0);
|
||||
let e = self.config.index.timestamp_to_index(end).unwrap_or(0);
|
||||
self.range(s..e)
|
||||
}}
|
||||
}}
|
||||
|
||||
impl<T: DeserializeOwned> SingleItemBuilder<T> {{
|
||||
/// Builder for single item access.
|
||||
pub struct SingleItemBuilder<T, D = SeriesData<T>> {{
|
||||
config: EndpointConfig,
|
||||
_marker: std::marker::PhantomData<fn() -> (T, D)>,
|
||||
}}
|
||||
|
||||
/// Date-aware single item builder.
|
||||
pub type DateSingleItemBuilder<T> = SingleItemBuilder<T, DateSeriesData<T>>;
|
||||
|
||||
impl<T: DeserializeOwned, D: DeserializeOwned> SingleItemBuilder<T, D> {{
|
||||
/// Fetch the single item.
|
||||
pub fn fetch(self) -> Result<MetricData<T>> {{
|
||||
pub fn fetch(self) -> Result<D> {{
|
||||
self.config.get_json(None)
|
||||
}}
|
||||
|
||||
@@ -322,21 +388,24 @@ impl<T: DeserializeOwned> SingleItemBuilder<T> {{
|
||||
}}
|
||||
|
||||
/// Builder after calling `skip(n)`. Chain with `take(n)` to specify count.
|
||||
pub struct SkippedBuilder<T> {{
|
||||
pub struct SkippedBuilder<T, D = SeriesData<T>> {{
|
||||
config: EndpointConfig,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
_marker: std::marker::PhantomData<fn() -> (T, D)>,
|
||||
}}
|
||||
|
||||
impl<T: DeserializeOwned> SkippedBuilder<T> {{
|
||||
/// Date-aware skipped builder.
|
||||
pub type DateSkippedBuilder<T> = SkippedBuilder<T, DateSeriesData<T>>;
|
||||
|
||||
impl<T: DeserializeOwned, D: DeserializeOwned> SkippedBuilder<T, D> {{
|
||||
/// Take n items after the skipped position.
|
||||
pub fn take(mut self, n: usize) -> RangeBuilder<T> {{
|
||||
pub fn take(mut self, n: usize) -> RangeBuilder<T, D> {{
|
||||
let start = self.config.start.unwrap_or(0);
|
||||
self.config.end = Some(start + n as i64);
|
||||
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||
}}
|
||||
|
||||
/// Fetch from the skipped position to the end.
|
||||
pub fn fetch(self) -> Result<MetricData<T>> {{
|
||||
pub fn fetch(self) -> Result<D> {{
|
||||
self.config.get_json(None)
|
||||
}}
|
||||
|
||||
@@ -347,14 +416,17 @@ impl<T: DeserializeOwned> SkippedBuilder<T> {{
|
||||
}}
|
||||
|
||||
/// Builder with range fully specified.
|
||||
pub struct RangeBuilder<T> {{
|
||||
pub struct RangeBuilder<T, D = SeriesData<T>> {{
|
||||
config: EndpointConfig,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
_marker: std::marker::PhantomData<fn() -> (T, D)>,
|
||||
}}
|
||||
|
||||
impl<T: DeserializeOwned> RangeBuilder<T> {{
|
||||
/// Date-aware range builder.
|
||||
pub type DateRangeBuilder<T> = RangeBuilder<T, DateSeriesData<T>>;
|
||||
|
||||
impl<T: DeserializeOwned, D: DeserializeOwned> RangeBuilder<T, D> {{
|
||||
/// Fetch the range as parsed JSON.
|
||||
pub fn fetch(self) -> Result<MetricData<T>> {{
|
||||
pub fn fetch(self) -> Result<D> {{
|
||||
self.config.get_json(None)
|
||||
}}
|
||||
|
||||
@@ -389,12 +461,17 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
|
||||
}
|
||||
writeln!(output).unwrap();
|
||||
|
||||
// Generate helper function
|
||||
// Generate helper functions
|
||||
writeln!(
|
||||
output,
|
||||
r#"#[inline]
|
||||
fn _ep<T: DeserializeOwned>(c: &Arc<BrkClientBase>, n: &Arc<str>, i: Index) -> MetricEndpointBuilder<T> {{
|
||||
MetricEndpointBuilder::new(c.clone(), n.clone(), i)
|
||||
fn _ep<T: DeserializeOwned>(c: &Arc<BrkClientBase>, n: &Arc<str>, i: Index) -> SeriesEndpoint<T> {{
|
||||
SeriesEndpoint::new(c.clone(), n.clone(), i)
|
||||
}}
|
||||
|
||||
#[inline]
|
||||
fn _dep<T: DeserializeOwned>(c: &Arc<BrkClientBase>, n: &Arc<str>, i: Index) -> DateSeriesEndpoint<T> {{
|
||||
DateSeriesEndpoint::new(c.clone(), n.clone(), i)
|
||||
}}
|
||||
"#
|
||||
)
|
||||
@@ -412,12 +489,21 @@ fn _ep<T: DeserializeOwned>(c: &Arc<BrkClientBase>, n: &Arc<str>, i: Index) -> M
|
||||
writeln!(output, "impl<T: DeserializeOwned> {}<T> {{", by_name).unwrap();
|
||||
for index in &pattern.indexes {
|
||||
let method_name = index_to_field_name(index);
|
||||
writeln!(
|
||||
output,
|
||||
" pub fn {}(&self) -> MetricEndpointBuilder<T> {{ _ep(&self.client, &self.name, Index::{}) }}",
|
||||
method_name, index
|
||||
)
|
||||
.unwrap();
|
||||
if index.is_date_based() {
|
||||
writeln!(
|
||||
output,
|
||||
" pub fn {}(&self) -> DateSeriesEndpoint<T> {{ _dep(&self.client, &self.name, Index::{}) }}",
|
||||
method_name, index
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
output,
|
||||
" pub fn {}(&self) -> SeriesEndpoint<T> {{ _ep(&self.client, &self.name, Index::{}) }}",
|
||||
method_name, index
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
|
||||
@@ -438,18 +524,18 @@ fn _ep<T: DeserializeOwned>(c: &Arc<BrkClientBase>, n: &Arc<str>, i: Index) -> M
|
||||
writeln!(output, " pub fn name(&self) -> &str {{ &self.name }}").unwrap();
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
|
||||
// Implement AnyMetricPattern trait
|
||||
// Implement AnySeriesPattern trait
|
||||
writeln!(
|
||||
output,
|
||||
"impl<T> AnyMetricPattern for {}<T> {{ fn name(&self) -> &str {{ &self.name }} fn indexes(&self) -> &'static [Index] {{ {} }} }}",
|
||||
"impl<T> AnySeriesPattern for {}<T> {{ fn name(&self) -> &str {{ &self.name }} fn indexes(&self) -> &'static [Index] {{ {} }} }}",
|
||||
pattern.name, idx_const
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Implement MetricPattern<T> trait
|
||||
// Implement SeriesPattern<T> trait
|
||||
writeln!(
|
||||
output,
|
||||
"impl<T: DeserializeOwned> MetricPattern<T> for {}<T> {{ fn get(&self, index: Index) -> Option<MetricEndpointBuilder<T>> {{ {}.contains(&index).then(|| _ep(&self.by.client, &self.by.name, index)) }} }}\n",
|
||||
"impl<T: DeserializeOwned> SeriesPattern<T> for {}<T> {{ fn get(&self, index: Index) -> Option<SeriesEndpoint<T>> {{ {}.contains(&index).then(|| _ep(&self.by.client, &self.by.name, index)) }} }}\n",
|
||||
pattern.name, idx_const
|
||||
)
|
||||
.unwrap();
|
||||
@@ -476,7 +562,7 @@ pub fn generate_pattern_structs(
|
||||
writeln!(output, "pub struct {}{} {{", pattern.name, generic_params).unwrap();
|
||||
|
||||
for field in &pattern.fields {
|
||||
let field_name = to_snake_case(&field.name);
|
||||
let field_name = escape_rust_keyword(&to_snake_case(&field.name));
|
||||
let type_annotation = metadata.field_type_annotation(
|
||||
field,
|
||||
pattern.is_generic,
|
||||
@@ -488,7 +574,11 @@ pub fn generate_pattern_structs(
|
||||
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
|
||||
// Generate impl block with constructor for ALL patterns
|
||||
// Skip constructor for non-parameterizable patterns (inlined at tree level)
|
||||
if !metadata.is_parameterizable(&pattern.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let impl_generic = if pattern.is_generic {
|
||||
"<T: DeserializeOwned>"
|
||||
} else {
|
||||
@@ -503,14 +593,22 @@ pub fn generate_pattern_structs(
|
||||
|
||||
writeln!(
|
||||
output,
|
||||
" /// Create a new pattern node with accumulated metric name."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {{"
|
||||
" /// Create a new pattern node with accumulated series name."
|
||||
)
|
||||
.unwrap();
|
||||
if pattern.is_templated() {
|
||||
writeln!(
|
||||
output,
|
||||
" pub fn new(client: Arc<BrkClientBase>, acc: String, disc: String) -> Self {{"
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
output,
|
||||
" pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {{"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
writeln!(output, " Self {{").unwrap();
|
||||
|
||||
let syntax = RustSyntax;
|
||||
|
||||
@@ -7,8 +7,9 @@ pub mod client;
|
||||
pub mod tree;
|
||||
mod types;
|
||||
|
||||
use std::{fmt::Write, fs, io, path::Path};
|
||||
use std::{fmt::Write, io, path::Path};
|
||||
|
||||
use super::write_if_changed;
|
||||
use crate::{ClientMetadata, Endpoint};
|
||||
|
||||
/// Generate Rust client from metadata and OpenAPI endpoints.
|
||||
@@ -24,6 +25,7 @@ pub fn generate_rust_client(
|
||||
writeln!(output, "// Auto-generated BRK Rust client").unwrap();
|
||||
writeln!(output, "// Do not edit manually\n").unwrap();
|
||||
writeln!(output, "#![allow(non_camel_case_types)]").unwrap();
|
||||
writeln!(output, "#![allow(non_snake_case)]").unwrap();
|
||||
writeln!(output, "#![allow(dead_code)]").unwrap();
|
||||
writeln!(output, "#![allow(unused_variables)]").unwrap();
|
||||
writeln!(output, "#![allow(clippy::useless_format)]").unwrap();
|
||||
@@ -31,14 +33,14 @@ pub fn generate_rust_client(
|
||||
|
||||
client::generate_imports(&mut output);
|
||||
client::generate_base_client(&mut output);
|
||||
client::generate_metric_pattern_trait(&mut output);
|
||||
client::generate_series_pattern_trait(&mut output);
|
||||
client::generate_endpoint(&mut output);
|
||||
client::generate_index_accessors(&mut output, &metadata.index_set_patterns);
|
||||
client::generate_pattern_structs(&mut output, &metadata.structural_patterns, metadata);
|
||||
tree::generate_tree(&mut output, &metadata.catalog, metadata);
|
||||
api::generate_main_client(&mut output, endpoints);
|
||||
|
||||
fs::write(output_path, output)?;
|
||||
write_if_changed(output_path, &output)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
//! Rust tree structure generation.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::collections::BTreeSet;
|
||||
use std::fmt::Write;
|
||||
|
||||
use brk_types::TreeNode;
|
||||
|
||||
use crate::{
|
||||
ClientMetadata, GenericSyntax, LanguageSyntax, PatternField, RustSyntax, build_child_path,
|
||||
generate_leaf_field, generate_tree_node_field, prepare_tree_node, to_snake_case,
|
||||
escape_rust_keyword, generate_leaf_field, generate_tree_node_field, prepare_tree_node,
|
||||
to_snake_case,
|
||||
};
|
||||
|
||||
/// Generate tree structs.
|
||||
pub fn generate_tree(output: &mut String, catalog: &TreeNode, metadata: &ClientMetadata) {
|
||||
writeln!(output, "// Metrics tree\n").unwrap();
|
||||
writeln!(output, "// Series tree\n").unwrap();
|
||||
|
||||
let pattern_lookup = metadata.pattern_lookup();
|
||||
let mut generated = HashSet::new();
|
||||
let mut generated = BTreeSet::new();
|
||||
generate_tree_node(
|
||||
output,
|
||||
"MetricsTree",
|
||||
"SeriesTree",
|
||||
"",
|
||||
catalog,
|
||||
&pattern_lookup,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
&mut generated,
|
||||
);
|
||||
@@ -32,30 +33,24 @@ fn generate_tree_node(
|
||||
name: &str,
|
||||
path: &str,
|
||||
node: &TreeNode,
|
||||
pattern_lookup: &std::collections::HashMap<Vec<PatternField>, String>,
|
||||
pattern_lookup: &std::collections::BTreeMap<Vec<PatternField>, String>,
|
||||
metadata: &ClientMetadata,
|
||||
generated: &mut HashSet<String>,
|
||||
generated: &mut BTreeSet<String>,
|
||||
) {
|
||||
let Some(ctx) = prepare_tree_node(node, name, path, pattern_lookup, metadata, generated) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Generate struct definition
|
||||
writeln!(output, "/// Metrics tree node.").unwrap();
|
||||
writeln!(output, "/// Series tree node.").unwrap();
|
||||
writeln!(output, "pub struct {} {{", name).unwrap();
|
||||
|
||||
for child in &ctx.children {
|
||||
let field_name = to_snake_case(child.name);
|
||||
let field_name = escape_rust_keyword(&to_snake_case(child.name));
|
||||
let type_annotation = if child.should_inline {
|
||||
child.inline_type_name.clone()
|
||||
} else {
|
||||
metadata.resolve_tree_field_type(
|
||||
&child.field,
|
||||
child.child_fields.as_deref(),
|
||||
name,
|
||||
child.name,
|
||||
GenericSyntax::RUST,
|
||||
)
|
||||
metadata.field_type_annotation(&child.field, false, None, GenericSyntax::RUST)
|
||||
};
|
||||
writeln!(output, " pub {}: {},", field_name, type_annotation).unwrap();
|
||||
}
|
||||
@@ -73,7 +68,7 @@ fn generate_tree_node(
|
||||
|
||||
let syntax = RustSyntax;
|
||||
for child in &ctx.children {
|
||||
let field_name = to_snake_case(child.name);
|
||||
let field_name = escape_rust_keyword(&to_snake_case(child.name));
|
||||
|
||||
if child.is_leaf {
|
||||
if let TreeNode::Leaf(leaf) = child.node {
|
||||
@@ -97,17 +92,14 @@ fn generate_tree_node(
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
// Pattern type - use ::new() constructor
|
||||
// All patterns have ::new(), parameterizable ones use detected mode,
|
||||
// non-parameterizable ones use field name fallback
|
||||
generate_tree_node_field(
|
||||
output,
|
||||
&syntax,
|
||||
&child.field,
|
||||
metadata,
|
||||
" ",
|
||||
child.name,
|
||||
Some(&child.base_result.base),
|
||||
"client.clone()",
|
||||
&child.base_result,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ pub fn js_type_to_rust(js_type: &str) -> String {
|
||||
"integer" => "i64".to_string(),
|
||||
"number" => "f64".to_string(),
|
||||
"boolean" => "bool".to_string(),
|
||||
"*" => "serde_json::Value".to_string(),
|
||||
"*" | "Object" => "serde_json::Value".to_string(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,340 +0,0 @@
|
||||
use std::{collections::BTreeMap, io};
|
||||
|
||||
use crate::ref_to_type_name;
|
||||
use oas3::Spec;
|
||||
use oas3::spec::{
|
||||
ObjectOrReference, ObjectSchema, Operation, ParameterIn, PathItem, Schema, SchemaType,
|
||||
SchemaTypeSet,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
/// Type schema extracted from OpenAPI components
|
||||
pub type TypeSchemas = BTreeMap<String, Value>;
|
||||
|
||||
/// Endpoint information extracted from OpenAPI spec
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Endpoint {
|
||||
/// HTTP method (GET, POST, etc.)
|
||||
pub method: String,
|
||||
/// Path template (e.g., "/blocks/{hash}")
|
||||
pub path: String,
|
||||
/// Operation ID (e.g., "getBlockByHash")
|
||||
pub operation_id: Option<String>,
|
||||
/// Short summary
|
||||
pub summary: Option<String>,
|
||||
/// Detailed description
|
||||
pub description: Option<String>,
|
||||
/// Path parameters
|
||||
pub path_params: Vec<Parameter>,
|
||||
/// Query parameters
|
||||
pub query_params: Vec<Parameter>,
|
||||
/// Response type (simplified)
|
||||
pub response_type: Option<String>,
|
||||
/// Whether this endpoint is deprecated
|
||||
pub deprecated: bool,
|
||||
/// Whether this endpoint supports CSV format (text/csv content type)
|
||||
pub supports_csv: bool,
|
||||
}
|
||||
|
||||
impl Endpoint {
|
||||
/// Returns true if this endpoint should be included in client generation.
|
||||
/// Only non-deprecated GET endpoints are included.
|
||||
pub fn should_generate(&self) -> bool {
|
||||
self.method == "GET" && !self.deprecated
|
||||
}
|
||||
|
||||
/// Returns the operation ID or generates one from the path.
|
||||
/// The returned string uses the raw case from the spec (typically camelCase).
|
||||
pub fn operation_name(&self) -> String {
|
||||
if let Some(op_id) = &self.operation_id {
|
||||
return op_id.clone();
|
||||
}
|
||||
// Generate from path: /api/block/{hash} -> "get_block"
|
||||
// Skip "api" prefix, convert hyphens to underscores, avoid redundant param names
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
let mut prev_segment = "";
|
||||
|
||||
for segment in self.path.split('/').filter(|s| !s.is_empty()) {
|
||||
if segment == "api" {
|
||||
continue;
|
||||
}
|
||||
if let Some(param) = segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
|
||||
// Only add "by_{param}" if the previous segment doesn't already contain the param name
|
||||
let prev_normalized = prev_segment.replace('-', "_");
|
||||
if !prev_normalized.ends_with(param) {
|
||||
parts.push(format!("by_{}", param));
|
||||
}
|
||||
} else {
|
||||
let normalized = segment.replace('-', "_");
|
||||
parts.push(normalized);
|
||||
prev_segment = segment;
|
||||
}
|
||||
}
|
||||
format!("get_{}", parts.join("_"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameter information
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Parameter {
|
||||
pub name: String,
|
||||
pub required: bool,
|
||||
pub param_type: String,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
/// Parse OpenAPI spec from JSON string
|
||||
///
|
||||
/// Pre-processes the JSON to handle oas3 limitations:
|
||||
/// - Removes unsupported siblings from `$ref` objects (oas3 only supports `summary` and `description`)
|
||||
pub fn parse_openapi_json(json: &str) -> io::Result<Spec> {
|
||||
let mut value: Value =
|
||||
serde_json::from_str(json).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
|
||||
// Clean up for oas3 compatibility
|
||||
clean_for_oas3(&mut value);
|
||||
|
||||
let cleaned_json =
|
||||
serde_json::to_string(&value).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
|
||||
oas3::from_json(&cleaned_json).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
|
||||
}
|
||||
|
||||
/// Extract type schemas from OpenAPI JSON
|
||||
pub fn extract_schemas(json: &str) -> TypeSchemas {
|
||||
let Ok(value) = serde_json::from_str::<Value>(json) else {
|
||||
return BTreeMap::new();
|
||||
};
|
||||
|
||||
value
|
||||
.get("components")
|
||||
.and_then(|c| c.get("schemas"))
|
||||
.and_then(|s| s.as_object())
|
||||
.map(|schemas| {
|
||||
schemas
|
||||
.iter()
|
||||
.map(|(name, schema)| (name.clone(), schema.clone()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Clean up OpenAPI spec for oas3 compatibility.
|
||||
/// - Removes unsupported siblings from $ref objects (oas3 only supports summary and description)
|
||||
/// - Converts boolean schemas to object schemas (oas3 doesn't handle `"schema": true`)
|
||||
fn clean_for_oas3(value: &mut Value) {
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
// Handle $ref with unsupported siblings
|
||||
if map.contains_key("$ref") {
|
||||
map.retain(|k, _| k == "$ref" || k == "summary" || k == "description");
|
||||
} else {
|
||||
// Convert boolean schemas to empty object schemas
|
||||
if let Some(schema) = map.get_mut("schema")
|
||||
&& schema.is_boolean()
|
||||
{
|
||||
*schema = Value::Object(serde_json::Map::new());
|
||||
}
|
||||
for v in map.values_mut() {
|
||||
clean_for_oas3(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::Array(arr) => {
|
||||
for v in arr {
|
||||
clean_for_oas3(v);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract all endpoints from OpenAPI spec
|
||||
pub fn extract_endpoints(spec: &Spec) -> Vec<Endpoint> {
|
||||
let mut endpoints = Vec::new();
|
||||
|
||||
let Some(paths) = &spec.paths else {
|
||||
return endpoints;
|
||||
};
|
||||
|
||||
for (path, path_item) in paths {
|
||||
for (method, operation) in get_operations(path_item) {
|
||||
if let Some(endpoint) = extract_endpoint(path, method, operation) {
|
||||
endpoints.push(endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endpoints
|
||||
}
|
||||
|
||||
fn get_operations(path_item: &PathItem) -> Vec<(&'static str, &Operation)> {
|
||||
[
|
||||
("GET", &path_item.get),
|
||||
("POST", &path_item.post),
|
||||
("PUT", &path_item.put),
|
||||
("DELETE", &path_item.delete),
|
||||
("PATCH", &path_item.patch),
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|(method, op)| op.as_ref().map(|o| (method, o)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn extract_endpoint(path: &str, method: &str, operation: &Operation) -> Option<Endpoint> {
|
||||
let path_params = extract_path_parameters(path, operation);
|
||||
let query_params = extract_parameters(operation, ParameterIn::Query);
|
||||
|
||||
let response_type = extract_response_type(operation);
|
||||
let supports_csv = check_csv_support(operation);
|
||||
|
||||
Some(Endpoint {
|
||||
method: method.to_string(),
|
||||
path: path.to_string(),
|
||||
operation_id: operation.operation_id.clone(),
|
||||
summary: operation.summary.clone(),
|
||||
description: operation.description.clone(),
|
||||
path_params,
|
||||
query_params,
|
||||
response_type,
|
||||
deprecated: operation.deprecated.unwrap_or(false),
|
||||
supports_csv,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if the endpoint supports CSV format (has text/csv in 200 response content types).
|
||||
fn check_csv_support(operation: &Operation) -> bool {
|
||||
let Some(responses) = operation.responses.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
let Some(response) = responses.get("200") else {
|
||||
return false;
|
||||
};
|
||||
match response {
|
||||
ObjectOrReference::Object(response) => response.content.contains_key("text/csv"),
|
||||
ObjectOrReference::Ref { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract path parameters in the order they appear in the path URL.
|
||||
fn extract_path_parameters(path: &str, operation: &Operation) -> Vec<Parameter> {
|
||||
// Extract parameter names from the path in order (e.g., "/api/metric/{metric}/{index}" -> ["metric", "index"])
|
||||
let path_order: Vec<&str> = path
|
||||
.split('/')
|
||||
.filter_map(|segment| segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')))
|
||||
.collect();
|
||||
|
||||
// Get all path parameters from the operation
|
||||
let params = extract_parameters(operation, ParameterIn::Path);
|
||||
|
||||
// Sort by position in the path
|
||||
let mut sorted_params: Vec<Parameter> = params;
|
||||
sorted_params.sort_by_key(|p| {
|
||||
path_order
|
||||
.iter()
|
||||
.position(|&name| name == p.name)
|
||||
.unwrap_or(usize::MAX)
|
||||
});
|
||||
|
||||
sorted_params
|
||||
}
|
||||
|
||||
fn extract_parameters(operation: &Operation, location: ParameterIn) -> Vec<Parameter> {
|
||||
operation
|
||||
.parameters
|
||||
.iter()
|
||||
.filter_map(|p| match p {
|
||||
ObjectOrReference::Object(param) if param.location == location => {
|
||||
let param_type = param
|
||||
.schema
|
||||
.as_ref()
|
||||
.and_then(|s| match s {
|
||||
ObjectOrReference::Ref { ref_path, .. } => {
|
||||
ref_to_type_name(ref_path).map(|s| s.to_string())
|
||||
}
|
||||
ObjectOrReference::Object(obj_schema) => schema_to_type_name(obj_schema),
|
||||
})
|
||||
.unwrap_or_else(|| "string".to_string());
|
||||
Some(Parameter {
|
||||
name: param.name.clone(),
|
||||
required: param.required.unwrap_or(false),
|
||||
param_type,
|
||||
description: param.description.clone(),
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn extract_response_type(operation: &Operation) -> Option<String> {
|
||||
let responses = operation.responses.as_ref()?;
|
||||
|
||||
// Look for 200 OK response
|
||||
let response = responses.get("200")?;
|
||||
|
||||
match response {
|
||||
ObjectOrReference::Object(response) => {
|
||||
// Look for JSON content
|
||||
let content = response.content.get("application/json")?;
|
||||
|
||||
match &content.schema {
|
||||
Some(ObjectOrReference::Ref { ref_path, .. }) => {
|
||||
// Extract type name from reference like "#/components/schemas/Block"
|
||||
Some(ref_to_type_name(ref_path)?.to_string())
|
||||
}
|
||||
Some(ObjectOrReference::Object(schema)) => schema_to_type_name(schema),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
ObjectOrReference::Ref { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn schema_type_from_schema(schema: &Schema) -> Option<String> {
|
||||
match schema {
|
||||
Schema::Boolean(_) => Some("boolean".to_string()),
|
||||
Schema::Object(obj_or_ref) => match obj_or_ref.as_ref() {
|
||||
ObjectOrReference::Object(obj_schema) => schema_to_type_name(obj_schema),
|
||||
ObjectOrReference::Ref { ref_path, .. } => {
|
||||
// Return the type name as-is (e.g., "Height", "Address")
|
||||
// These should have definitions generated from schemas
|
||||
ref_to_type_name(ref_path).map(|s| s.to_string())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn schema_to_type_name(schema: &ObjectSchema) -> Option<String> {
|
||||
let schema_type = schema.schema_type.as_ref()?;
|
||||
|
||||
match schema_type {
|
||||
SchemaTypeSet::Single(t) => single_type_to_name(t, schema),
|
||||
SchemaTypeSet::Multiple(types) => {
|
||||
// For nullable types like ["integer", "null"], return the non-null type
|
||||
types
|
||||
.iter()
|
||||
.find(|t| !matches!(t, SchemaType::Null))
|
||||
.and_then(|t| single_type_to_name(t, schema))
|
||||
.or(Some("*".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn single_type_to_name(t: &SchemaType, schema: &ObjectSchema) -> Option<String> {
|
||||
match t {
|
||||
SchemaType::String => Some("string".to_string()),
|
||||
SchemaType::Number => Some("number".to_string()),
|
||||
SchemaType::Integer => Some("number".to_string()),
|
||||
SchemaType::Boolean => Some("boolean".to_string()),
|
||||
SchemaType::Array => {
|
||||
let inner = match &schema.items {
|
||||
Some(boxed_schema) => schema_type_from_schema(boxed_schema),
|
||||
None => Some("*".to_string()),
|
||||
};
|
||||
inner.map(|t| format!("{}[]", t))
|
||||
}
|
||||
SchemaType::Object => Some("Object".to_string()),
|
||||
SchemaType::Null => Some("null".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
use crate::openapi::{Parameter, ResponseKind};
|
||||
|
||||
/// Request body shape for POST/PUT/PATCH endpoints.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RequestBody {
|
||||
/// Body content type as a name (e.g. "string" for text/plain, "Foo" for an `application/json` $ref).
|
||||
pub body_type: String,
|
||||
/// Whether the body is required.
|
||||
pub required: bool,
|
||||
}
|
||||
|
||||
/// Endpoint information extracted from OpenAPI spec.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Endpoint {
|
||||
/// HTTP method (GET, POST, etc.)
|
||||
pub method: String,
|
||||
/// Path template (e.g., "/blocks/{hash}")
|
||||
pub path: String,
|
||||
/// Operation ID (e.g., "getBlockByHash")
|
||||
pub operation_id: Option<String>,
|
||||
/// Short summary
|
||||
pub summary: Option<String>,
|
||||
/// Detailed description
|
||||
pub description: Option<String>,
|
||||
/// Path parameters
|
||||
pub path_params: Vec<Parameter>,
|
||||
/// Query parameters
|
||||
pub query_params: Vec<Parameter>,
|
||||
/// Request body, if any (POST/PUT/PATCH).
|
||||
pub request_body: Option<RequestBody>,
|
||||
/// Body kind for the 200 response.
|
||||
pub response_kind: ResponseKind,
|
||||
/// Whether this endpoint is deprecated
|
||||
pub deprecated: bool,
|
||||
/// Whether this endpoint supports CSV format (text/csv content type)
|
||||
pub supports_csv: bool,
|
||||
}
|
||||
|
||||
impl Endpoint {
|
||||
/// Returns true if this endpoint should be included in client generation.
|
||||
/// Non-deprecated GET and POST endpoints are included.
|
||||
pub fn should_generate(&self) -> bool {
|
||||
!self.deprecated && (self.method == "GET" || self.method == "POST")
|
||||
}
|
||||
|
||||
/// Returns true if this endpoint returns JSON.
|
||||
pub fn returns_json(&self) -> bool {
|
||||
matches!(self.response_kind, ResponseKind::Json(_))
|
||||
}
|
||||
|
||||
/// Returns true if this endpoint returns binary data (application/octet-stream).
|
||||
pub fn returns_binary(&self) -> bool {
|
||||
matches!(self.response_kind, ResponseKind::Binary)
|
||||
}
|
||||
|
||||
/// Returns true if this endpoint returns plain text (typed or opaque).
|
||||
pub fn returns_text(&self) -> bool {
|
||||
matches!(self.response_kind, ResponseKind::Text(_))
|
||||
}
|
||||
|
||||
/// Schema name attached to the response, if any.
|
||||
pub fn schema_name(&self) -> Option<&str> {
|
||||
self.response_kind.schema_name()
|
||||
}
|
||||
|
||||
/// Returns the operation ID or generates one from the path.
|
||||
/// The returned string uses the raw case from the spec (typically camelCase).
|
||||
pub fn operation_name(&self) -> String {
|
||||
if let Some(op_id) = &self.operation_id {
|
||||
return op_id.clone();
|
||||
}
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
let mut prev_segment = "";
|
||||
|
||||
for segment in self.path.split('/').filter(|s| !s.is_empty()) {
|
||||
if segment == "api" {
|
||||
continue;
|
||||
}
|
||||
if let Some(param) = segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
|
||||
let prev_normalized = prev_segment.replace('-', "_");
|
||||
if !prev_normalized.ends_with(param) {
|
||||
parts.push(format!("by_{}", param));
|
||||
}
|
||||
} else {
|
||||
let normalized = segment.replace('-', "_");
|
||||
parts.push(normalized);
|
||||
prev_segment = segment;
|
||||
}
|
||||
}
|
||||
format!("get_{}", parts.join("_"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
mod endpoint;
|
||||
mod parameter;
|
||||
mod response_kind;
|
||||
mod text_schema;
|
||||
|
||||
pub use endpoint::{Endpoint, RequestBody};
|
||||
pub use parameter::Parameter;
|
||||
pub use response_kind::ResponseKind;
|
||||
pub use text_schema::TextSchema;
|
||||
|
||||
use std::{collections::BTreeMap, io};
|
||||
|
||||
use crate::ref_to_type_name;
|
||||
use oas3::Spec;
|
||||
use oas3::spec::{
|
||||
ObjectOrReference, ObjectSchema, Operation, ParameterIn, PathItem, Schema, SchemaType,
|
||||
SchemaTypeSet,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
/// Type schema extracted from OpenAPI components
|
||||
pub type TypeSchemas = BTreeMap<String, Value>;
|
||||
|
||||
/// Parse OpenAPI spec from JSON string
|
||||
///
|
||||
/// Pre-processes the JSON to handle oas3 limitations:
|
||||
/// - Removes unsupported siblings from `$ref` objects (oas3 only supports `summary` and `description`)
|
||||
pub fn parse_openapi_json(json: &str) -> io::Result<Spec> {
|
||||
let mut value: Value =
|
||||
serde_json::from_str(json).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
|
||||
// Clean up for oas3 compatibility
|
||||
clean_for_oas3(&mut value);
|
||||
|
||||
let cleaned_json =
|
||||
serde_json::to_string(&value).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
|
||||
oas3::from_json(&cleaned_json).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
|
||||
}
|
||||
|
||||
/// Extract type schemas from OpenAPI JSON
|
||||
pub fn extract_schemas(json: &str) -> TypeSchemas {
|
||||
let Ok(value) = serde_json::from_str::<Value>(json) else {
|
||||
return BTreeMap::new();
|
||||
};
|
||||
|
||||
value
|
||||
.get("components")
|
||||
.and_then(|c| c.get("schemas"))
|
||||
.and_then(|s| s.as_object())
|
||||
.map(|schemas| {
|
||||
schemas
|
||||
.iter()
|
||||
.map(|(name, schema)| (name.clone(), schema.clone()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Clean up OpenAPI spec for oas3 compatibility.
|
||||
/// - Removes unsupported siblings from $ref objects (oas3 only supports summary and description)
|
||||
/// - Converts boolean schemas to object schemas (oas3 doesn't handle `"schema": true`)
|
||||
fn clean_for_oas3(value: &mut Value) {
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
// Handle $ref with unsupported siblings
|
||||
if map.contains_key("$ref") {
|
||||
map.retain(|k, _| k == "$ref" || k == "summary" || k == "description");
|
||||
} else {
|
||||
// Convert boolean schemas to empty object schemas
|
||||
if let Some(schema) = map.get_mut("schema")
|
||||
&& schema.is_boolean()
|
||||
{
|
||||
*schema = Value::Object(serde_json::Map::new());
|
||||
}
|
||||
for v in map.values_mut() {
|
||||
clean_for_oas3(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::Array(arr) => {
|
||||
for v in arr {
|
||||
clean_for_oas3(v);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract all endpoints from OpenAPI spec
|
||||
pub fn extract_endpoints(spec: &Spec) -> Vec<Endpoint> {
|
||||
let mut endpoints = Vec::new();
|
||||
|
||||
let Some(paths) = &spec.paths else {
|
||||
return endpoints;
|
||||
};
|
||||
|
||||
for (path, path_item) in paths {
|
||||
for (method, operation) in get_operations(path_item) {
|
||||
if let Some(endpoint) = extract_endpoint(path, method, operation, spec) {
|
||||
endpoints.push(endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endpoints
|
||||
}
|
||||
|
||||
fn get_operations(path_item: &PathItem) -> Vec<(&'static str, &Operation)> {
|
||||
[
|
||||
("GET", &path_item.get),
|
||||
("POST", &path_item.post),
|
||||
("PUT", &path_item.put),
|
||||
("DELETE", &path_item.delete),
|
||||
("PATCH", &path_item.patch),
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|(method, op)| op.as_ref().map(|o| (method, o)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn extract_endpoint(
|
||||
path: &str,
|
||||
method: &str,
|
||||
operation: &Operation,
|
||||
spec: &Spec,
|
||||
) -> Option<Endpoint> {
|
||||
let path_params = extract_path_parameters(path, operation);
|
||||
let query_params = extract_parameters(operation, ParameterIn::Query);
|
||||
|
||||
let response_kind = extract_response_kind(operation, spec);
|
||||
let request_body = extract_request_body(operation);
|
||||
let supports_csv = check_csv_support(operation);
|
||||
|
||||
Some(Endpoint {
|
||||
method: method.to_string(),
|
||||
path: path.to_string(),
|
||||
operation_id: operation.operation_id.clone(),
|
||||
summary: operation.summary.clone(),
|
||||
description: operation.description.clone(),
|
||||
path_params,
|
||||
query_params,
|
||||
request_body,
|
||||
response_kind,
|
||||
deprecated: operation.deprecated.unwrap_or(false),
|
||||
supports_csv,
|
||||
})
|
||||
}
|
||||
|
||||
/// Extract the request body shape, if any.
|
||||
/// Prefers `text/plain` (string) over `application/json` (typed).
|
||||
fn extract_request_body(operation: &Operation) -> Option<RequestBody> {
|
||||
let req = operation.request_body.as_ref()?;
|
||||
let req = match req {
|
||||
ObjectOrReference::Object(rb) => rb,
|
||||
ObjectOrReference::Ref { .. } => return None,
|
||||
};
|
||||
|
||||
let body_type = if req.content.contains_key("text/plain; charset=utf-8")
|
||||
|| req.content.contains_key("text/plain")
|
||||
{
|
||||
"string".to_string()
|
||||
} else if let Some(content) = req.content.get("application/json") {
|
||||
schema_name_from_content(content).unwrap_or_else(|| "Object".to_string())
|
||||
} else {
|
||||
"string".to_string()
|
||||
};
|
||||
|
||||
Some(RequestBody {
|
||||
body_type,
|
||||
required: req.required.unwrap_or(false),
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if the endpoint supports CSV format (has text/csv in 200 response content types).
|
||||
fn check_csv_support(operation: &Operation) -> bool {
|
||||
let Some(responses) = operation.responses.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
let Some(response) = responses.get("200") else {
|
||||
return false;
|
||||
};
|
||||
match response {
|
||||
ObjectOrReference::Object(response) => response.content.contains_key("text/csv"),
|
||||
ObjectOrReference::Ref { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract path parameters in the order they appear in the path URL.
|
||||
fn extract_path_parameters(path: &str, operation: &Operation) -> Vec<Parameter> {
|
||||
// Extract parameter names from the path in order (e.g., "/api/series/{series}/{index}" -> ["series", "index"])
|
||||
let path_order: Vec<&str> = path
|
||||
.split('/')
|
||||
.filter_map(|segment| segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')))
|
||||
.collect();
|
||||
|
||||
// Get all path parameters from the operation
|
||||
let params = extract_parameters(operation, ParameterIn::Path);
|
||||
|
||||
// Sort by position in the path
|
||||
let mut sorted_params: Vec<Parameter> = params;
|
||||
sorted_params.sort_by_key(|p| {
|
||||
path_order
|
||||
.iter()
|
||||
.position(|&name| name == p.name)
|
||||
.unwrap_or(usize::MAX)
|
||||
});
|
||||
|
||||
sorted_params
|
||||
}
|
||||
|
||||
fn extract_parameters(operation: &Operation, location: ParameterIn) -> Vec<Parameter> {
|
||||
operation
|
||||
.parameters
|
||||
.iter()
|
||||
.filter_map(|p| match p {
|
||||
ObjectOrReference::Object(param) if param.location == location => {
|
||||
let param_type = param
|
||||
.schema
|
||||
.as_ref()
|
||||
.and_then(schema_type_from_schema)
|
||||
.unwrap_or_else(|| "string".to_string());
|
||||
Some(Parameter {
|
||||
name: param.name.clone(),
|
||||
required: param.required.unwrap_or(false),
|
||||
param_type,
|
||||
description: param.description.clone(),
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn extract_response_kind(operation: &Operation, spec: &Spec) -> ResponseKind {
|
||||
let response = operation
|
||||
.responses
|
||||
.as_ref()
|
||||
.and_then(|r| r.get("200"))
|
||||
.and_then(|r| match r {
|
||||
ObjectOrReference::Object(o) => Some(o),
|
||||
ObjectOrReference::Ref { .. } => None,
|
||||
});
|
||||
let Some(response) = response else {
|
||||
return ResponseKind::Text(None);
|
||||
};
|
||||
|
||||
if response.content.contains_key("application/octet-stream") {
|
||||
return ResponseKind::Binary;
|
||||
}
|
||||
if let Some(content) = response.content.get("application/json") {
|
||||
return ResponseKind::Json(
|
||||
schema_name_from_content(content).unwrap_or_else(|| "*".to_string()),
|
||||
);
|
||||
}
|
||||
if let Some(content) = response.content.get("text/plain; charset=utf-8") {
|
||||
let schema = schema_name_from_content(content).map(|name| {
|
||||
let is_numeric = is_numeric_schema(spec, &name);
|
||||
TextSchema { name, is_numeric }
|
||||
});
|
||||
return ResponseKind::Text(schema);
|
||||
}
|
||||
ResponseKind::Text(None)
|
||||
}
|
||||
|
||||
fn schema_name_from_content(content: &oas3::spec::MediaType) -> Option<String> {
|
||||
schema_type_from_schema(content.schema.as_ref()?)
|
||||
}
|
||||
|
||||
/// Resolves `name` against `components.schemas` and reports whether the
|
||||
/// underlying primitive is `integer` or `number`.
|
||||
fn is_numeric_schema(spec: &Spec, name: &str) -> bool {
|
||||
let Some(components) = spec.components.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
let Some(Schema::Object(obj_or_ref)) = components.schemas.get(name) else {
|
||||
return false;
|
||||
};
|
||||
let ObjectOrReference::Object(schema) = obj_or_ref.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
matches!(
|
||||
schema.schema_type.as_ref(),
|
||||
Some(SchemaTypeSet::Single(
|
||||
SchemaType::Integer | SchemaType::Number
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
fn schema_type_from_schema(schema: &Schema) -> Option<String> {
|
||||
match schema {
|
||||
Schema::Boolean(_) => Some("boolean".to_string()),
|
||||
Schema::Object(obj_or_ref) => match obj_or_ref.as_ref() {
|
||||
ObjectOrReference::Object(obj_schema) => schema_to_type_name(obj_schema),
|
||||
ObjectOrReference::Ref { ref_path, .. } => {
|
||||
// Return the type name as-is (e.g., "Height", "Address")
|
||||
// These should have definitions generated from schemas
|
||||
ref_to_type_name(ref_path).map(|s| s.to_string())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn schema_to_type_name(schema: &ObjectSchema) -> Option<String> {
|
||||
if let Some(schema_type) = schema.schema_type.as_ref() {
|
||||
return match schema_type {
|
||||
SchemaTypeSet::Single(t) => single_type_to_name(t, schema),
|
||||
SchemaTypeSet::Multiple(types) => {
|
||||
// For nullable types like ["integer", "null"], return the non-null type
|
||||
types
|
||||
.iter()
|
||||
.find(|t| !matches!(t, SchemaType::Null))
|
||||
.and_then(|t| single_type_to_name(t, schema))
|
||||
.or(Some("*".to_string()))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Handle anyOf/oneOf unions (e.g., Option<RangeIndex> → anyOf: [$ref, null])
|
||||
let variants = if !schema.any_of.is_empty() {
|
||||
&schema.any_of
|
||||
} else if !schema.one_of.is_empty() {
|
||||
&schema.one_of
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let types: Vec<String> = variants
|
||||
.iter()
|
||||
.filter_map(|v| match v {
|
||||
Schema::Boolean(_) => None,
|
||||
Schema::Object(obj_or_ref) => match obj_or_ref.as_ref() {
|
||||
ObjectOrReference::Ref { ref_path, .. } => {
|
||||
ref_to_type_name(ref_path).map(|s| s.to_string())
|
||||
}
|
||||
ObjectOrReference::Object(obj) => {
|
||||
if matches!(
|
||||
obj.schema_type.as_ref(),
|
||||
Some(SchemaTypeSet::Single(SchemaType::Null))
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
schema_to_type_name(obj)
|
||||
}
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
||||
match types.len() {
|
||||
0 => None,
|
||||
1 => Some(types.into_iter().next().unwrap()),
|
||||
_ => Some(types.join(" | ")),
|
||||
}
|
||||
}
|
||||
|
||||
fn single_type_to_name(t: &SchemaType, schema: &ObjectSchema) -> Option<String> {
|
||||
match t {
|
||||
SchemaType::String => Some("string".to_string()),
|
||||
SchemaType::Number => Some("number".to_string()),
|
||||
SchemaType::Integer => Some("integer".to_string()),
|
||||
SchemaType::Boolean => Some("boolean".to_string()),
|
||||
SchemaType::Array => {
|
||||
let inner = match &schema.items {
|
||||
Some(boxed_schema) => schema_type_from_schema(boxed_schema),
|
||||
None => Some("*".to_string()),
|
||||
};
|
||||
inner.map(|t| format!("{}[]", t))
|
||||
}
|
||||
SchemaType::Object => Some("Object".to_string()),
|
||||
SchemaType::Null => Some("null".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/// Parameter information.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Parameter {
|
||||
pub name: String,
|
||||
pub required: bool,
|
||||
pub param_type: String,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use crate::openapi::TextSchema;
|
||||
|
||||
/// 200-response body shape.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ResponseKind {
|
||||
/// JSON body, schema named (e.g. "Block").
|
||||
Json(String),
|
||||
/// `text/plain` body. `Some(schema)` carries a typed shape (e.g. "Height", "Hex");
|
||||
/// `None` is the escape hatch for opaque text.
|
||||
Text(Option<TextSchema>),
|
||||
/// `application/octet-stream`.
|
||||
Binary,
|
||||
}
|
||||
|
||||
impl ResponseKind {
|
||||
/// Schema name, if the body is named (Json or typed Text).
|
||||
pub fn schema_name(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Json(s) => Some(s.as_str()),
|
||||
Self::Text(Some(t)) => Some(t.name.as_str()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// True when a typed text body needs numeric parsing (`int(...)` etc.).
|
||||
pub fn text_is_numeric(&self) -> bool {
|
||||
matches!(self, Self::Text(Some(t)) if t.is_numeric)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/// Schema metadata for a typed `text/plain` response.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TextSchema {
|
||||
/// Schema name, e.g. "Height", "Hex".
|
||||
pub name: String,
|
||||
/// True when the underlying primitive is `integer`/`number` (body needs numeric parsing).
|
||||
pub is_numeric: bool,
|
||||
}
|
||||
@@ -92,4 +92,31 @@ pub trait LanguageSyntax {
|
||||
/// - JavaScript: `createTypeName`
|
||||
/// - Rust: `TypeName::new`
|
||||
fn constructor_name(&self, type_name: &str) -> String;
|
||||
|
||||
/// Return a variable as an owned value expression.
|
||||
///
|
||||
/// - Rust: `var.clone()` (String needs explicit cloning)
|
||||
/// - JavaScript/Python: `var` (no ownership)
|
||||
fn owned_expr(&self, var: &str) -> String {
|
||||
var.to_string()
|
||||
}
|
||||
|
||||
/// Format a discriminator argument for passing to a templated child.
|
||||
///
|
||||
/// Returns an expression computing the disc value from a template.
|
||||
/// - `"pct99"` (static) → `'pct99'` (JS) / `"pct99".to_string()` (Rust)
|
||||
/// - `""` (empty) → `disc` (pass parent's disc through)
|
||||
/// - `"p1sd{disc}"` (suffix) → `_m('p1sd', disc)` (composed)
|
||||
/// - `"ratio_{disc}_bps"` (embedded) → `` `ratio_${disc}_bps` `` (template literal)
|
||||
fn disc_arg_expr(&self, template: &str) -> String;
|
||||
|
||||
/// Format a templated mode expression: substitute `{disc}` at runtime.
|
||||
///
|
||||
/// The template contains `{disc}` placeholder. The generated code should
|
||||
/// construct `_m(acc, template_with_disc_substituted)` at runtime.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `acc_var` - The accumulator variable (e.g., "acc")
|
||||
/// * `template` - Template like `"ratio_{disc}_bps"` or `"{disc}"`
|
||||
fn template_expr(&self, acc_var: &str, template: &str) -> String;
|
||||
}
|
||||
|
||||
@@ -14,25 +14,25 @@ pub fn to_pascal_case(s: &str) -> String {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Convert a string to snake_case, handling Rust keywords.
|
||||
/// Convert a string to snake_case (no keyword escaping — backends handle that).
|
||||
pub fn to_snake_case(s: &str) -> String {
|
||||
// Convert to lowercase and replace dashes with underscores
|
||||
let sanitized = s.to_lowercase().replace('-', "_");
|
||||
|
||||
// Prefix with _ if starts with digit
|
||||
let sanitized = if sanitized.chars().next().is_some_and(|c| c.is_ascii_digit()) {
|
||||
if sanitized.chars().next().is_some_and(|c| c.is_ascii_digit()) {
|
||||
format!("_{}", sanitized)
|
||||
} else {
|
||||
sanitized
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Rust keywords
|
||||
match sanitized.as_str() {
|
||||
/// Escape Rust reserved keywords with `_` suffix (consistent with Python).
|
||||
pub fn escape_rust_keyword(name: &str) -> String {
|
||||
match name {
|
||||
"type" | "const" | "static" | "match" | "if" | "else" | "loop" | "while" | "for"
|
||||
| "break" | "continue" | "return" | "fn" | "let" | "mut" | "ref" | "self" | "super"
|
||||
| "mod" | "use" | "pub" | "crate" | "extern" | "impl" | "trait" | "struct" | "enum"
|
||||
| "where" | "async" | "await" | "dyn" | "move" => format!("r#{}", sanitized),
|
||||
_ => sanitized,
|
||||
| "where" | "async" | "await" | "dyn" | "move" => format!("{}_", name),
|
||||
_ => name.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,9 +54,9 @@ pub fn to_camel_case(s: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an Index to a snake_case field name (e.g., DateIndex -> dateindex).
|
||||
/// Convert an Index to a snake_case field name (e.g., Day1 -> day1).
|
||||
pub fn index_to_field_name(index: &Index) -> String {
|
||||
to_snake_case(index.serialize_long())
|
||||
to_snake_case(index.name())
|
||||
}
|
||||
|
||||
/// Generate a child type/struct/class name (e.g., ParentName + child_name -> ParentName_ChildName).
|
||||
@@ -74,6 +74,9 @@ pub fn escape_python_keyword(name: &str) -> String {
|
||||
"try", "while", "with", "yield",
|
||||
];
|
||||
|
||||
// Strip characters invalid in identifiers (e.g. `[]` from `txId[]`)
|
||||
let name = name.replace(['[', ']'], "");
|
||||
|
||||
// Prefix with underscore if starts with digit
|
||||
let name = if name.starts_with(|c: char| c.is_ascii_digit()) {
|
||||
format!("_{}", name)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! Client metadata extracted from brk_query.
|
||||
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use brk_query::Vecs;
|
||||
use brk_types::{Index, MetricLeafWithSchema};
|
||||
use brk_types::{Index, SeriesLeafWithSchema};
|
||||
|
||||
use super::{GenericSyntax, IndexSetPattern, PatternField, StructuralPattern, extract_inner_type};
|
||||
use crate::{PatternBaseResult, analysis};
|
||||
@@ -15,14 +15,14 @@ pub struct ClientMetadata {
|
||||
pub catalog: brk_types::TreeNode,
|
||||
/// Structural patterns - tree node shapes that repeat
|
||||
pub structural_patterns: Vec<StructuralPattern>,
|
||||
/// Index set patterns - sets of indexes that appear together on metrics
|
||||
/// Index set patterns - sets of indexes that appear together on series
|
||||
pub index_set_patterns: Vec<IndexSetPattern>,
|
||||
/// Maps concrete field signatures to pattern names
|
||||
concrete_to_pattern: HashMap<Vec<PatternField>, String>,
|
||||
/// Maps field signatures to pattern names (merged from concrete instances + pattern definitions)
|
||||
pattern_lookup: BTreeMap<Vec<PatternField>, String>,
|
||||
/// Maps concrete field signatures to their type parameter (for generic patterns)
|
||||
concrete_to_type_param: HashMap<Vec<PatternField>, String>,
|
||||
concrete_to_type_param: BTreeMap<Vec<PatternField>, String>,
|
||||
/// Maps tree paths to their computed PatternBaseResult
|
||||
node_bases: HashMap<String, PatternBaseResult>,
|
||||
node_bases: BTreeMap<String, PatternBaseResult>,
|
||||
}
|
||||
|
||||
impl ClientMetadata {
|
||||
@@ -37,11 +37,17 @@ impl ClientMetadata {
|
||||
analysis::detect_structural_patterns(&catalog);
|
||||
let index_set_patterns = analysis::detect_index_patterns(&catalog);
|
||||
|
||||
// Build merged pattern lookup: concrete instances + pattern definitions
|
||||
let mut pattern_lookup = concrete_to_pattern;
|
||||
for p in &structural_patterns {
|
||||
pattern_lookup.insert(p.fields.clone(), p.name.clone());
|
||||
}
|
||||
|
||||
ClientMetadata {
|
||||
catalog,
|
||||
structural_patterns,
|
||||
index_set_patterns,
|
||||
concrete_to_pattern,
|
||||
pattern_lookup,
|
||||
concrete_to_type_param,
|
||||
node_bases,
|
||||
}
|
||||
@@ -54,78 +60,29 @@ impl ClientMetadata {
|
||||
.find(|p| &p.indexes == indexes)
|
||||
}
|
||||
|
||||
/// Check if a type is a structural pattern name.
|
||||
pub fn is_pattern_type(&self, type_name: &str) -> bool {
|
||||
self.structural_patterns.iter().any(|p| p.name == type_name)
|
||||
}
|
||||
|
||||
/// Find a pattern by name.
|
||||
pub fn find_pattern(&self, name: &str) -> Option<&StructuralPattern> {
|
||||
self.structural_patterns.iter().find(|p| p.name == name)
|
||||
}
|
||||
|
||||
/// Check if a pattern is generic.
|
||||
pub fn is_pattern_generic(&self, name: &str) -> bool {
|
||||
self.find_pattern(name).is_some_and(|p| p.is_generic)
|
||||
}
|
||||
|
||||
/// Check if a pattern by name is fully parameterizable.
|
||||
/// A pattern is parameterizable if it has a mode AND all its branch fields
|
||||
/// are also parameterizable (or not patterns at all).
|
||||
/// Check if a pattern is fully parameterizable (recursively).
|
||||
/// Returns false if the pattern or any nested branch pattern has no mode.
|
||||
pub fn is_parameterizable(&self, name: &str) -> bool {
|
||||
self.find_pattern(name).is_some_and(|p| {
|
||||
if !p.is_parameterizable() {
|
||||
return false;
|
||||
}
|
||||
// Check all branch fields have parameterizable types (or are not patterns)
|
||||
p.fields.iter().all(|f| {
|
||||
if f.is_branch() {
|
||||
self.structural_patterns
|
||||
.iter()
|
||||
.find(|pat| pat.name == f.rust_type)
|
||||
.is_none_or(|pat| pat.is_parameterizable())
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
p.is_parameterizable()
|
||||
&& p.fields.iter().all(|f| {
|
||||
!f.is_branch()
|
||||
|| self.find_pattern(&f.rust_type).is_none()
|
||||
|| self.is_parameterizable(&f.rust_type)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if child fields match ANY pattern (parameterizable or not).
|
||||
/// Used for type annotations - we want to reuse pattern types for all patterns.
|
||||
pub fn matches_pattern(&self, fields: &[PatternField]) -> bool {
|
||||
self.concrete_to_pattern.contains_key(fields)
|
||||
|| self.structural_patterns.iter().any(|p| p.fields == fields)
|
||||
}
|
||||
|
||||
/// Find a pattern by its fields.
|
||||
/// Find a pattern by its concrete fields.
|
||||
pub fn find_pattern_by_fields(&self, fields: &[PatternField]) -> Option<&StructuralPattern> {
|
||||
self.concrete_to_pattern
|
||||
self.pattern_lookup
|
||||
.get(fields)
|
||||
.and_then(|name| self.find_pattern(name))
|
||||
.or_else(|| self.structural_patterns.iter().find(|p| p.fields == fields))
|
||||
}
|
||||
|
||||
/// Resolve the type name for a tree field.
|
||||
/// If the field matches ANY pattern (parameterizable or not), returns pattern type.
|
||||
/// Otherwise returns the inline type name (parent_child format).
|
||||
pub fn resolve_tree_field_type(
|
||||
&self,
|
||||
field: &PatternField,
|
||||
child_fields: Option<&[PatternField]>,
|
||||
parent_name: &str,
|
||||
child_name: &str,
|
||||
syntax: GenericSyntax,
|
||||
) -> String {
|
||||
match child_fields {
|
||||
// Use pattern type for ANY matching pattern (parameterizable or not)
|
||||
Some(cf) if self.matches_pattern(cf) => {
|
||||
let generic_value_type = self.get_type_param(cf).map(String::as_str);
|
||||
self.field_type_annotation(field, false, generic_value_type, syntax)
|
||||
}
|
||||
Some(_) => crate::child_type_name(parent_name, child_name),
|
||||
None => self.field_type_annotation(field, false, None, syntax),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the type parameter for a generic pattern given its concrete fields.
|
||||
@@ -133,13 +90,9 @@ impl ClientMetadata {
|
||||
self.concrete_to_type_param.get(fields)
|
||||
}
|
||||
|
||||
/// Build a lookup map from field signatures to pattern names.
|
||||
pub fn pattern_lookup(&self) -> HashMap<Vec<PatternField>, String> {
|
||||
let mut lookup = self.concrete_to_pattern.clone();
|
||||
for p in &self.structural_patterns {
|
||||
lookup.insert(p.fields.clone(), p.name.clone());
|
||||
}
|
||||
lookup
|
||||
/// Get the pre-computed pattern lookup map.
|
||||
pub fn pattern_lookup(&self) -> &BTreeMap<Vec<PatternField>, String> {
|
||||
&self.pattern_lookup
|
||||
}
|
||||
|
||||
/// Get the pre-computed PatternBaseResult for a tree path.
|
||||
@@ -155,14 +108,9 @@ impl ClientMetadata {
|
||||
generic_value_type: Option<&str>,
|
||||
syntax: GenericSyntax,
|
||||
) -> String {
|
||||
let value_type = if is_generic && field.rust_type == "T" {
|
||||
"T".to_string()
|
||||
} else {
|
||||
extract_inner_type(&field.rust_type)
|
||||
};
|
||||
|
||||
if self.is_pattern_type(&field.rust_type) {
|
||||
if self.is_pattern_generic(&field.rust_type) {
|
||||
// Pattern type — single lookup instead of is_pattern_type + is_pattern_generic
|
||||
if let Some(pattern) = self.find_pattern(&field.rust_type) {
|
||||
if pattern.is_generic {
|
||||
let type_param = field
|
||||
.type_param
|
||||
.as_deref()
|
||||
@@ -170,30 +118,41 @@ impl ClientMetadata {
|
||||
.unwrap_or(if is_generic { "T" } else { syntax.default_type });
|
||||
return syntax.wrap(&field.rust_type, type_param);
|
||||
}
|
||||
field.rust_type.clone()
|
||||
} else if field.is_branch() {
|
||||
field.rust_type.clone()
|
||||
} else if let Some(accessor) = self.find_index_set_pattern(&field.indexes) {
|
||||
return field.rust_type.clone();
|
||||
}
|
||||
|
||||
// Branch type (non-pattern)
|
||||
if field.is_branch() {
|
||||
return field.rust_type.clone();
|
||||
}
|
||||
|
||||
// Leaf type
|
||||
let value_type = if is_generic && field.rust_type == "T" {
|
||||
"T".to_string()
|
||||
} else {
|
||||
extract_inner_type(&field.rust_type)
|
||||
};
|
||||
if let Some(accessor) = self.find_index_set_pattern(&field.indexes) {
|
||||
syntax.wrap(&accessor.name, &value_type)
|
||||
} else {
|
||||
syntax.wrap("MetricNode", &value_type)
|
||||
syntax.wrap("SeriesNode", &value_type)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate type annotation for a leaf node with language-specific syntax.
|
||||
///
|
||||
/// This is a simpler version of `field_type_annotation` that works directly
|
||||
/// with a `MetricLeafWithSchema` node instead of a `PatternField`.
|
||||
/// with a `SeriesLeafWithSchema` node instead of a `PatternField`.
|
||||
pub fn field_type_annotation_from_leaf(
|
||||
&self,
|
||||
leaf: &MetricLeafWithSchema,
|
||||
leaf: &SeriesLeafWithSchema,
|
||||
syntax: GenericSyntax,
|
||||
) -> String {
|
||||
let value_type = leaf.kind().to_string();
|
||||
if let Some(accessor) = self.find_index_set_pattern(leaf.indexes()) {
|
||||
syntax.wrap(&accessor.name, &value_type)
|
||||
} else {
|
||||
syntax.wrap("MetricNode", &value_type)
|
||||
syntax.wrap("SeriesNode", &value_type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
//! Pattern mode and field parts for metric name reconstruction.
|
||||
//! Pattern mode and field parts for series name reconstruction.
|
||||
//!
|
||||
//! Patterns are either suffix mode or prefix mode:
|
||||
//! - Suffix mode: `_m(acc, relative)` → `acc_relative` or just `relative` if acc empty
|
||||
//! - Prefix mode: `_p(prefix, acc)` → `prefix_acc` or just `acc` if prefix empty
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// How a pattern constructs metric names from the accumulator.
|
||||
/// How a pattern constructs series names from the accumulator.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PatternMode {
|
||||
/// Fields append their relative name to acc.
|
||||
/// Formula: `_m(acc, relative)` → `{acc}_{relative}` or `{relative}` if acc empty
|
||||
/// Example: `_m("lth", "max_cost_basis")` → `"lth_max_cost_basis"`
|
||||
Suffix {
|
||||
/// Maps field name to its relative name (full metric name when acc = "")
|
||||
relatives: HashMap<String, String>,
|
||||
/// Maps field name to its relative name (full series name when acc = "")
|
||||
relatives: BTreeMap<String, String>,
|
||||
},
|
||||
/// Fields prepend their prefix to acc.
|
||||
/// Formula: `_p(prefix, acc)` → `{prefix}_{acc}` or `{acc}` if prefix empty
|
||||
/// Example: `_p("cumulative", "lth_realized_loss")` → `"cumulative_lth_realized_loss"`
|
||||
Prefix {
|
||||
/// Maps field name to its prefix (empty string for identity)
|
||||
prefixes: HashMap<String, String>,
|
||||
prefixes: BTreeMap<String, String>,
|
||||
},
|
||||
/// Fields construct series names using a template with a discriminator placeholder.
|
||||
/// Factory takes two params: `acc` (base) and `disc` (discriminator).
|
||||
/// Formula: `_m(acc, template.replace("{disc}", disc))`
|
||||
/// Example: template `"ratio_{disc}_bps"` with disc `"pct99"` → `_m(acc, "ratio_pct99_bps")`
|
||||
Templated {
|
||||
/// Maps field name to its template string containing `{disc}` placeholder
|
||||
templates: BTreeMap<String, String>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
//! Structural pattern and field types.
|
||||
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use brk_types::Index;
|
||||
|
||||
use super::PatternMode;
|
||||
|
||||
/// A pattern of indexes that appear together on multiple metrics.
|
||||
/// A pattern of indexes that appear together on multiple series.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IndexSetPattern {
|
||||
/// Pattern name (e.g., "DateHeightIndexes")
|
||||
@@ -22,7 +22,7 @@ pub struct StructuralPattern {
|
||||
pub name: String,
|
||||
/// Ordered list of child fields
|
||||
pub fields: Vec<PatternField>,
|
||||
/// How fields construct metric names from acc (None = not parameterizable)
|
||||
/// How fields construct series names from acc (None = not parameterizable)
|
||||
pub mode: Option<PatternMode>,
|
||||
/// If true, all leaf fields use a type parameter T
|
||||
pub is_generic: bool,
|
||||
@@ -37,23 +37,57 @@ impl StructuralPattern {
|
||||
/// Get the field part (relative name or prefix) for a given field.
|
||||
pub fn get_field_part(&self, field_name: &str) -> Option<&str> {
|
||||
match &self.mode {
|
||||
Some(PatternMode::Suffix { relatives }) => relatives.get(field_name).map(|s| s.as_str()),
|
||||
Some(PatternMode::Suffix { relatives }) => {
|
||||
relatives.get(field_name).map(|s| s.as_str())
|
||||
}
|
||||
Some(PatternMode::Prefix { prefixes }) => prefixes.get(field_name).map(|s| s.as_str()),
|
||||
Some(PatternMode::Templated { templates }) => {
|
||||
templates.get(field_name).map(|s| s.as_str())
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this pattern is in suffix mode.
|
||||
pub fn is_suffix_mode(&self) -> bool {
|
||||
matches!(&self.mode, Some(PatternMode::Suffix { .. }))
|
||||
matches!(
|
||||
&self.mode,
|
||||
Some(PatternMode::Suffix { .. } | PatternMode::Templated { .. })
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns true if this pattern uses templated mode with a discriminator.
|
||||
pub fn is_templated(&self) -> bool {
|
||||
matches!(&self.mode, Some(PatternMode::Templated { .. }))
|
||||
}
|
||||
|
||||
/// Extract the discriminator value from a concrete instance's field_parts.
|
||||
/// Uses the pattern's templates to reverse-match and find the disc.
|
||||
pub fn extract_disc_from_instance(
|
||||
&self,
|
||||
instance_field_parts: &BTreeMap<String, String>,
|
||||
) -> Option<String> {
|
||||
let templates = match &self.mode {
|
||||
Some(PatternMode::Templated { templates }) => templates,
|
||||
_ => return None,
|
||||
};
|
||||
// Find a template with {disc} and extract the disc from the instance value.
|
||||
// Strip leading underscore since _m() handles separators.
|
||||
for (field_name, template) in templates {
|
||||
if let Some(value) = instance_field_parts.get(field_name)
|
||||
&& let Some(disc) = extract_disc(template, value)
|
||||
{
|
||||
return Some(disc.trim_start_matches('_').to_string());
|
||||
}
|
||||
}
|
||||
// If no template matched (all empty templates), disc is empty
|
||||
Some(String::new())
|
||||
}
|
||||
|
||||
/// Check if the given instance field parts match this pattern's field parts.
|
||||
/// Returns true if all field parts in the pattern match the instance's field parts.
|
||||
pub fn field_parts_match(&self, instance_field_parts: &HashMap<String, String>) -> bool {
|
||||
pub fn field_parts_match(&self, instance_field_parts: &BTreeMap<String, String>) -> bool {
|
||||
match &self.mode {
|
||||
Some(PatternMode::Suffix { relatives }) => {
|
||||
// For each field in the pattern, check if the instance has the same suffix
|
||||
relatives.iter().all(|(field_name, pattern_suffix)| {
|
||||
instance_field_parts
|
||||
.get(field_name)
|
||||
@@ -61,18 +95,51 @@ impl StructuralPattern {
|
||||
})
|
||||
}
|
||||
Some(PatternMode::Prefix { prefixes }) => {
|
||||
// For each field in the pattern, check if the instance has the same prefix
|
||||
prefixes.iter().all(|(field_name, pattern_prefix)| {
|
||||
instance_field_parts
|
||||
.get(field_name)
|
||||
.is_some_and(|instance_prefix| instance_prefix == pattern_prefix)
|
||||
})
|
||||
}
|
||||
None => false, // Non-parameterizable patterns don't use field parts
|
||||
Some(PatternMode::Templated { templates }) => {
|
||||
// For templated patterns, check if the instance's field_parts
|
||||
// can be produced by substituting some discriminator into the templates
|
||||
let first_template_field = templates.iter().next();
|
||||
let Some((ref_field, ref_template)) = first_template_field else {
|
||||
return false;
|
||||
};
|
||||
let Some(ref_value) = instance_field_parts.get(ref_field) else {
|
||||
return false;
|
||||
};
|
||||
// Extract discriminator from the reference field
|
||||
let Some(disc) = extract_disc(ref_template, ref_value) else {
|
||||
return false;
|
||||
};
|
||||
// Verify all fields match with this discriminator
|
||||
templates.iter().all(|(field_name, template)| {
|
||||
instance_field_parts
|
||||
.get(field_name)
|
||||
.is_some_and(|value| *value == template.replace("{disc}", &disc))
|
||||
})
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the discriminator value by matching a template against a concrete string.
|
||||
/// E.g., template `"ratio_{disc}_bps"` matched against `"ratio_pct99_bps"` yields `"pct99"`.
|
||||
fn extract_disc(template: &str, value: &str) -> Option<String> {
|
||||
let (prefix, suffix) = template.split_once("{disc}")?;
|
||||
if value.starts_with(prefix) && value.ends_with(suffix) {
|
||||
let disc = &value[prefix.len()..value.len() - suffix.len()];
|
||||
if !disc.is_empty() {
|
||||
return Some(disc.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// A field in a structural pattern.
|
||||
#[derive(Debug, Clone, PartialOrd, Ord)]
|
||||
pub struct PatternField {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,9 +12,7 @@ anyhow = "1.0"
|
||||
brk_alloc = { workspace = true }
|
||||
brk_computer = { workspace = true }
|
||||
brk_error = { workspace = true, features = ["tokio", "vecdb"] }
|
||||
brk_fetcher = { workspace = true }
|
||||
brk_indexer = { workspace = true }
|
||||
brk_iterator = { workspace = true }
|
||||
brk_logger = { workspace = true }
|
||||
brk_mempool = { workspace = true }
|
||||
brk_query = { workspace = true }
|
||||
@@ -23,10 +21,11 @@ brk_rpc = { workspace = true }
|
||||
brk_server = { workspace = true }
|
||||
brk_types = { workspace = true }
|
||||
lexopt = "0.3"
|
||||
owo-colors = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
toml = "0.9.11"
|
||||
toml = "1.1.2"
|
||||
vecdb = { workspace = true }
|
||||
|
||||
[[bin]]
|
||||
|
||||
+33
-12
@@ -1,26 +1,29 @@
|
||||
# brk_cli
|
||||
# BRK CLI
|
||||
|
||||
Command-line interface for running a Bitcoin Research Kit instance.
|
||||
Run your own Bitcoin Research Kit instance. One binary, one command. Full sync in ~4-7h depending on hardware. ~44% disk overhead vs 250% for mempool/electrs.
|
||||
|
||||
## Preview
|
||||
|
||||
- https://bitview.space - web interface
|
||||
- https://bitview.space/api - API docs
|
||||
[bitview.space](https://bitview.space) is the official free hosted instance.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Bitcoin Core running with RPC enabled
|
||||
- Linux or macOS
|
||||
- Bitcoin Core with `server=1` in `bitcoin.conf`
|
||||
- Access to `blk*.dat` files
|
||||
- ~400 GB disk space
|
||||
- 12+ GB RAM
|
||||
- [~400 GB disk space](https://bitview.space/api/server/disk) (see [Disk usage](#disk-usage))
|
||||
- [12+ GB RAM](https://github.com/bitcoinresearchkit/benches#benchmarks)
|
||||
|
||||
## Disk usage
|
||||
|
||||
BRK uses [sparse files](https://en.wikipedia.org/wiki/Sparse_file). Tools like `ls -l` or Finder report the logical file size (>1 TB), not actual disk usage (~350 GB). Use `du -sh` to see real usage.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
rustup update
|
||||
RUSTFLAGS="-C target-cpu=native" cargo install --locked brk_cli --version "$(cargo search brk_cli | head -1 | awk -F'"' '{print $2}')"
|
||||
rustup update && RUSTFLAGS="-C target-cpu=native" cargo install --locked brk_cli --version $(cargo search brk_cli | head -1 | awk -F'"' '{print $2}')
|
||||
```
|
||||
|
||||
Updates Rust, then builds `brk_cli` with optimizations tuned to your CPU. The `--version $(...)` subshell queries crates.io for the absolute latest published version, including pre-releases (rc/beta/alpha); without it, `cargo install` only picks the latest stable.
|
||||
|
||||
Portable build (without native CPU optimizations):
|
||||
|
||||
```bash
|
||||
@@ -35,6 +38,10 @@ brk
|
||||
|
||||
Indexes the blockchain, computes datasets, starts the server on `localhost:3110`, and waits for new blocks.
|
||||
|
||||
## First sync
|
||||
|
||||
The initial sync processes the entire blockchain and can take several hours. During this time (more than 10,000 blocks behind), indexing completes before the server starts to free up memory. The web interface at `localhost:3110` won't be available until sync finishes.
|
||||
|
||||
## Options
|
||||
|
||||
```bash
|
||||
@@ -42,7 +49,21 @@ brk -h # Show all options
|
||||
brk -V # Show version
|
||||
```
|
||||
|
||||
Options are saved to `~/.brk/config.toml` after first use.
|
||||
Command-line options override `~/.brk/config.toml` for that run only. Edit the file directly to persist settings:
|
||||
|
||||
```toml
|
||||
brkdir = "/path/to/data"
|
||||
bitcoindir = "/path/to/.bitcoin"
|
||||
```
|
||||
|
||||
All fields are optional. See `brk -h` for the full list.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
LOG=debug brk # Enable debug logging (keeps noise filters)
|
||||
RUST_LOG=... brk # Full control over log filtering (overrides all defaults)
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
|
||||
+185
-95
@@ -1,53 +1,58 @@
|
||||
use std::{
|
||||
fs,
|
||||
fs, io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use brk_error::{Error, Result};
|
||||
use brk_fetcher::Fetcher;
|
||||
use brk_rpc::{Auth, Client};
|
||||
use brk_server::{CdnCacheMode, DEFAULT_MAX_UTXOS, DEFAULT_MAX_WEIGHT, Website};
|
||||
use brk_types::Port;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use owo_colors::OwoColorize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{default_brk_path, dot_brk_path, fix_user_path, website::WebsiteArg};
|
||||
use crate::{default_brk_path, dot_brk_path, fix_user_path};
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[serde(default)]
|
||||
brkdir: Option<String>,
|
||||
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[serde(default)]
|
||||
brkport: Option<Port>,
|
||||
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
website: Option<WebsiteArg>,
|
||||
#[serde(default)]
|
||||
website: Option<Website>,
|
||||
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
fetch: Option<bool>,
|
||||
#[serde(default)]
|
||||
cdn: Option<bool>,
|
||||
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[serde(default)]
|
||||
maxweight: Option<usize>,
|
||||
|
||||
#[serde(default)]
|
||||
maxutxos: Option<usize>,
|
||||
|
||||
#[serde(default)]
|
||||
bitcoindir: Option<String>,
|
||||
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[serde(default)]
|
||||
blocksdir: Option<String>,
|
||||
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[serde(default)]
|
||||
rpcconnect: Option<String>,
|
||||
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[serde(default)]
|
||||
rpcport: Option<u16>,
|
||||
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[serde(default)]
|
||||
rpccookiefile: Option<String>,
|
||||
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[serde(default)]
|
||||
rpcuser: Option<String>,
|
||||
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[serde(default)]
|
||||
rpcpassword: Option<String>,
|
||||
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
check_collisions: Option<bool>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -71,8 +76,14 @@ impl Config {
|
||||
if let Some(v) = config_args.website {
|
||||
config.website = Some(v);
|
||||
}
|
||||
if let Some(v) = config_args.fetch {
|
||||
config.fetch = Some(v);
|
||||
if let Some(v) = config_args.cdn {
|
||||
config.cdn = Some(v);
|
||||
}
|
||||
if let Some(v) = config_args.maxweight {
|
||||
config.maxweight = Some(v);
|
||||
}
|
||||
if let Some(v) = config_args.maxutxos {
|
||||
config.maxutxos = Some(v);
|
||||
}
|
||||
if let Some(v) = config_args.bitcoindir {
|
||||
config.bitcoindir = Some(v);
|
||||
@@ -95,14 +106,9 @@ impl Config {
|
||||
if let Some(v) = config_args.rpcpassword {
|
||||
config.rpcpassword = Some(v);
|
||||
}
|
||||
if let Some(v) = config_args.check_collisions {
|
||||
config.check_collisions = Some(v);
|
||||
}
|
||||
|
||||
config.check();
|
||||
|
||||
config.write(&path)?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
@@ -125,15 +131,30 @@ impl Config {
|
||||
Long("brkdir") => config.brkdir = Some(parser.value().unwrap().parse().unwrap()),
|
||||
Long("brkport") => config.brkport = Some(parser.value().unwrap().parse().unwrap()),
|
||||
Long("website") => config.website = Some(parser.value().unwrap().parse().unwrap()),
|
||||
Long("fetch") => config.fetch = Some(parser.value().unwrap().parse().unwrap()),
|
||||
Long("bitcoindir") => config.bitcoindir = Some(parser.value().unwrap().parse().unwrap()),
|
||||
Long("blocksdir") => config.blocksdir = Some(parser.value().unwrap().parse().unwrap()),
|
||||
Long("rpcconnect") => config.rpcconnect = Some(parser.value().unwrap().parse().unwrap()),
|
||||
Long("cdn") => config.cdn = Some(parser.value().unwrap().parse().unwrap()),
|
||||
Long("maxweight") => {
|
||||
config.maxweight = Some(parser.value().unwrap().parse().unwrap())
|
||||
}
|
||||
Long("maxutxos") => {
|
||||
config.maxutxos = Some(parser.value().unwrap().parse().unwrap())
|
||||
}
|
||||
Long("bitcoindir") => {
|
||||
config.bitcoindir = Some(parser.value().unwrap().parse().unwrap())
|
||||
}
|
||||
Long("blocksdir") => {
|
||||
config.blocksdir = Some(parser.value().unwrap().parse().unwrap())
|
||||
}
|
||||
Long("rpcconnect") => {
|
||||
config.rpcconnect = Some(parser.value().unwrap().parse().unwrap())
|
||||
}
|
||||
Long("rpcport") => config.rpcport = Some(parser.value().unwrap().parse().unwrap()),
|
||||
Long("rpccookiefile") => config.rpccookiefile = Some(parser.value().unwrap().parse().unwrap()),
|
||||
Long("rpccookiefile") => {
|
||||
config.rpccookiefile = Some(parser.value().unwrap().parse().unwrap())
|
||||
}
|
||||
Long("rpcuser") => config.rpcuser = Some(parser.value().unwrap().parse().unwrap()),
|
||||
Long("rpcpassword") => config.rpcpassword = Some(parser.value().unwrap().parse().unwrap()),
|
||||
Long("check-collisions") => config.check_collisions = Some(parser.value().unwrap().parse().unwrap()),
|
||||
Long("rpcpassword") => {
|
||||
config.rpcpassword = Some(parser.value().unwrap().parse().unwrap())
|
||||
}
|
||||
_ => {
|
||||
eprintln!("{}", arg.unexpected());
|
||||
std::process::exit(1);
|
||||
@@ -145,31 +166,108 @@ impl Config {
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
let v = env!("CARGO_PKG_VERSION");
|
||||
|
||||
println!("{} {}", "brk".bold(), v.bright_black());
|
||||
println!("Bitcoin Research Kit");
|
||||
println!();
|
||||
println!("{}", "USAGE:".bold());
|
||||
println!(
|
||||
"brk {}
|
||||
Bitcoin Research Kit
|
||||
|
||||
USAGE:
|
||||
brk [OPTIONS]
|
||||
|
||||
OPTIONS:
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
|
||||
--brkdir <PATH> Output directory [~/.brk]
|
||||
--brkport <PORT> Server port [3110]
|
||||
--website <BOOL|PATH> Website: true, false, or path [true]
|
||||
--fetch <BOOL> Fetch prices [true]
|
||||
|
||||
--bitcoindir <PATH> Bitcoin directory [~/.bitcoin, ~/Library/Application Support/Bitcoin]
|
||||
--blocksdir <PATH> Blocks directory [<bitcoindir>/blocks]
|
||||
|
||||
--rpcconnect <IP> RPC host [localhost]
|
||||
--rpcport <PORT> RPC port [8332]
|
||||
--rpccookiefile <PATH> RPC cookie file [<bitcoindir>/.cookie]
|
||||
--rpcuser <USERNAME> RPC username
|
||||
--rpcpassword <PASSWORD> RPC password",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
" {} brk {}",
|
||||
"[ENV]".bright_black(),
|
||||
"[OPTIONS]".bright_black()
|
||||
);
|
||||
println!();
|
||||
println!("{}", "OPTIONS:".bold());
|
||||
println!(" -h, --help Print help");
|
||||
println!(" -V, --version Print version");
|
||||
println!();
|
||||
println!(
|
||||
" --brkdir {} Output directory {}",
|
||||
"<PATH>".bright_black(),
|
||||
"[~/.brk]".bright_black()
|
||||
);
|
||||
println!(
|
||||
" --brkport {} Server port {}",
|
||||
"<PORT>".bright_black(),
|
||||
"[3110]".bright_black()
|
||||
);
|
||||
println!(
|
||||
" --website {} Website {}",
|
||||
"<BOOL|PATH>".bright_black(),
|
||||
"[true]".bright_black()
|
||||
);
|
||||
println!(
|
||||
" --cdn {} Aggressive CDN cache, requires purge on deploy {}",
|
||||
"<BOOL>".bright_black(),
|
||||
"[false]".bright_black()
|
||||
);
|
||||
println!(
|
||||
" --maxweight {} Server cap on series response weight in bytes; rejects /api/{{series,metric}}/... over the limit {}",
|
||||
"<BYTES>".bright_black(),
|
||||
format!("[{}]", DEFAULT_MAX_WEIGHT).bright_black()
|
||||
);
|
||||
println!(
|
||||
" --maxutxos {} Server cap on UTXOs per address; /api/address/{{addr}}/utxo errors past the limit {}",
|
||||
"<COUNT>".bright_black(),
|
||||
format!("[{}]", DEFAULT_MAX_UTXOS).bright_black()
|
||||
);
|
||||
println!();
|
||||
println!(
|
||||
" --bitcoindir {} Bitcoin directory {}",
|
||||
"<PATH>".bright_black(),
|
||||
"[OS default]".bright_black()
|
||||
);
|
||||
println!(
|
||||
" --blocksdir {} Blocks directory {}",
|
||||
"<PATH>".bright_black(),
|
||||
"[<bitcoindir>/blocks]".bright_black()
|
||||
);
|
||||
println!();
|
||||
println!(
|
||||
" --rpcconnect {} RPC host {}",
|
||||
"<IP>".bright_black(),
|
||||
"[localhost]".bright_black()
|
||||
);
|
||||
println!(
|
||||
" --rpcport {} RPC port {}",
|
||||
"<PORT>".bright_black(),
|
||||
"[8332]".bright_black()
|
||||
);
|
||||
println!(
|
||||
" --rpccookiefile {} RPC cookie file {}",
|
||||
"<PATH>".bright_black(),
|
||||
"[<bitcoindir>/.cookie]".bright_black()
|
||||
);
|
||||
println!(
|
||||
" --rpcuser {} RPC username",
|
||||
"<USERNAME>".bright_black()
|
||||
);
|
||||
println!(
|
||||
" --rpcpassword {} RPC password",
|
||||
"<PASSWORD>".bright_black()
|
||||
);
|
||||
println!();
|
||||
println!("{}", "ENVIRONMENT:".bold());
|
||||
println!(
|
||||
" LOG={} Log level {}",
|
||||
"<LEVEL>".bright_black(),
|
||||
"[info]".bright_black()
|
||||
);
|
||||
println!(
|
||||
" RUST_LOG={} Full log filter",
|
||||
"<RULES>".bright_black()
|
||||
);
|
||||
println!();
|
||||
println!("{}", "CONFIG:".bold());
|
||||
println!(
|
||||
" Edit {} to persist settings:",
|
||||
"~/.brk/config.toml".bright_black()
|
||||
);
|
||||
println!(" {}", "brkdir = \"/path/to/data\"".bright_black());
|
||||
println!(
|
||||
" {}",
|
||||
"bitcoindir = \"/path/to/.bitcoin\"".bright_black()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -206,14 +304,18 @@ Finally, you can run the program with '-h' for help."
|
||||
}
|
||||
|
||||
fn read(path: &Path) -> Self {
|
||||
fs::read_to_string(path).map_or_else(
|
||||
|_| Config::default(),
|
||||
|contents| toml::from_str(&contents).unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
fn write(&self, path: &Path) -> std::io::Result<()> {
|
||||
fs::write(path, toml::to_string(self).unwrap())
|
||||
let contents = match fs::read_to_string(path) {
|
||||
Ok(contents) => contents,
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => return Config::default(),
|
||||
Err(e) => {
|
||||
eprintln!("Cannot read {}: {e}", path.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
toml::from_str(&contents).unwrap_or_else(|e| {
|
||||
eprintln!("Invalid {}:\n{e}", path.display());
|
||||
std::process::exit(1);
|
||||
})
|
||||
}
|
||||
|
||||
pub fn rpc(&self) -> Result<Client> {
|
||||
@@ -269,10 +371,6 @@ Finally, you can run the program with '-h' for help."
|
||||
.map_or_else(default_brk_path, |s| fix_user_path(s.as_ref()))
|
||||
}
|
||||
|
||||
pub fn harsdir(&self) -> PathBuf {
|
||||
self.brkdir().join("hars")
|
||||
}
|
||||
|
||||
fn path_cookiefile(&self) -> PathBuf {
|
||||
self.rpccookiefile.as_ref().map_or_else(
|
||||
|| self.bitcoindir().join(".cookie"),
|
||||
@@ -280,35 +378,27 @@ Finally, you can run the program with '-h' for help."
|
||||
)
|
||||
}
|
||||
|
||||
pub fn website(&self) -> WebsiteArg {
|
||||
pub fn website(&self) -> Website {
|
||||
self.website.clone().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn cdn_cache_mode(&self) -> CdnCacheMode {
|
||||
if self.cdn.unwrap_or(false) {
|
||||
CdnCacheMode::Aggressive
|
||||
} else {
|
||||
CdnCacheMode::Live
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_weight(&self) -> usize {
|
||||
self.maxweight.unwrap_or(DEFAULT_MAX_WEIGHT)
|
||||
}
|
||||
|
||||
pub fn max_utxos(&self) -> usize {
|
||||
self.maxutxos.unwrap_or(DEFAULT_MAX_UTXOS)
|
||||
}
|
||||
|
||||
pub fn brkport(&self) -> Option<Port> {
|
||||
self.brkport
|
||||
}
|
||||
|
||||
pub fn fetch(&self) -> bool {
|
||||
self.fetch.is_none_or(|b| b)
|
||||
}
|
||||
|
||||
pub fn fetcher(&self) -> Option<Fetcher> {
|
||||
self.fetch()
|
||||
.then(|| Fetcher::import(Some(self.harsdir().as_path())).unwrap())
|
||||
}
|
||||
|
||||
pub fn check_collisions(&self) -> bool {
|
||||
self.check_collisions.is_some_and(|b| b)
|
||||
}
|
||||
}
|
||||
|
||||
fn default_on_error<'de, D, T>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
T: Deserialize<'de> + Default,
|
||||
{
|
||||
match T::deserialize(deserializer) {
|
||||
Ok(v) => Ok(v),
|
||||
Err(_) => Ok(T::default()),
|
||||
}
|
||||
}
|
||||
|
||||
+43
-32
@@ -3,37 +3,26 @@
|
||||
use std::{
|
||||
fs,
|
||||
thread::{self, sleep},
|
||||
time::Duration,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use brk_alloc::Mimalloc;
|
||||
use brk_computer::Computer;
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_iterator::Blocks;
|
||||
use brk_mempool::Mempool;
|
||||
use brk_query::AsyncQuery;
|
||||
use brk_reader::Reader;
|
||||
use brk_server::{Server, Website};
|
||||
use brk_server::{Server, ServerConfig};
|
||||
use tracing::info;
|
||||
use vecdb::Exit;
|
||||
|
||||
mod config;
|
||||
mod paths;
|
||||
mod website;
|
||||
|
||||
use crate::{config::Config, paths::*, website::WebsiteArg};
|
||||
use crate::{config::Config, paths::*};
|
||||
|
||||
pub fn main() -> anyhow::Result<()> {
|
||||
// Can't increase main thread's stack size, thus we need to use another thread
|
||||
thread::Builder::new()
|
||||
.stack_size(512 * 1024 * 1024)
|
||||
.spawn(run)?
|
||||
.join()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn run() -> anyhow::Result<()> {
|
||||
fs::create_dir_all(dot_brk_path())?;
|
||||
|
||||
brk_logger::init(Some(&dot_brk_log_path()))?;
|
||||
@@ -47,33 +36,50 @@ pub fn run() -> anyhow::Result<()> {
|
||||
|
||||
let reader = Reader::new(config.blocksdir(), &client);
|
||||
|
||||
let blocks = Blocks::new(&client, &reader);
|
||||
|
||||
let mut indexer = Indexer::forced_import(&config.brkdir())?;
|
||||
|
||||
let mut computer = Computer::forced_import(&config.brkdir(), &indexer, config.fetcher())?;
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
// Pre-run indexer if too far behind, then drop and reimport to reduce memory
|
||||
let chain_height = client.get_last_height()?;
|
||||
let indexed_height = indexer.vecs.next_height();
|
||||
let blocks_behind = chain_height.saturating_sub(*indexed_height);
|
||||
if blocks_behind > 10_000 {
|
||||
info!("---");
|
||||
info!("Indexing {blocks_behind} blocks before starting server...");
|
||||
info!("---");
|
||||
sleep(Duration::from_secs(10));
|
||||
indexer.index(&reader, &client, &exit)?;
|
||||
drop(indexer);
|
||||
Mimalloc::collect();
|
||||
indexer = Indexer::forced_import(&config.brkdir())?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut computer = Computer::forced_import(&config.brkdir(), &indexer)?;
|
||||
|
||||
let mempool = Mempool::new(&client);
|
||||
|
||||
let query = AsyncQuery::build(&reader, &indexer, &computer, Some(mempool.clone()));
|
||||
|
||||
let mempool_clone = mempool.clone();
|
||||
let resolver = query.sync(|q| q.indexer_prevout_resolver());
|
||||
thread::spawn(move || {
|
||||
mempool_clone.start();
|
||||
mempool_clone.start_with(resolver);
|
||||
});
|
||||
|
||||
let query = AsyncQuery::build(&reader, &indexer, &computer, Some(mempool));
|
||||
|
||||
let data_path = config.brkdir();
|
||||
|
||||
let website = match config.website() {
|
||||
WebsiteArg::Enabled(false) => Website::Disabled,
|
||||
WebsiteArg::Enabled(true) => Website::Default,
|
||||
WebsiteArg::Path(p) => Website::Filesystem(p),
|
||||
let server_config = ServerConfig {
|
||||
data_path: config.brkdir(),
|
||||
website: config.website(),
|
||||
cdn_cache_mode: config.cdn_cache_mode(),
|
||||
max_weight: config.max_weight(),
|
||||
max_utxos: config.max_utxos(),
|
||||
};
|
||||
|
||||
let port = config.brkport();
|
||||
|
||||
let future = async move {
|
||||
let server = Server::new(&query, data_path, website);
|
||||
let server = Server::new(&query, server_config);
|
||||
|
||||
tokio::spawn(async move {
|
||||
server.serve(port).await.unwrap();
|
||||
@@ -95,16 +101,21 @@ pub fn run() -> anyhow::Result<()> {
|
||||
|
||||
info!("{} blocks found.", u32::from(last_height) + 1);
|
||||
|
||||
let starting_indexes = if config.check_collisions() {
|
||||
indexer.checked_index(&blocks, &client, &exit)?
|
||||
let total_start = Instant::now();
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
indexer.checked_index(&reader, &client, &exit)?;
|
||||
} else {
|
||||
indexer.index(&blocks, &client, &exit)?
|
||||
};
|
||||
indexer.index(&reader, &client, &exit)?;
|
||||
}
|
||||
|
||||
Mimalloc::collect();
|
||||
|
||||
computer.compute(&indexer, starting_indexes, &reader, &exit)?;
|
||||
computer.compute(&indexer, &exit)?;
|
||||
|
||||
indexer.advance_safe_lengths()?;
|
||||
|
||||
info!("Total time: {:?}", total_start.elapsed());
|
||||
info!("Waiting for new blocks...");
|
||||
|
||||
while last_height == client.get_last_height()? {
|
||||
|
||||
@@ -6,7 +6,7 @@ pub fn dot_brk_path() -> PathBuf {
|
||||
}
|
||||
|
||||
pub fn dot_brk_log_path() -> PathBuf {
|
||||
dot_brk_path().join("log")
|
||||
dot_brk_path().join("logs")
|
||||
}
|
||||
|
||||
pub fn default_brk_path() -> PathBuf {
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
use std::{path::PathBuf, str::FromStr};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::paths::fix_user_path;
|
||||
|
||||
/// Website configuration:
|
||||
/// - `true` or omitted: serve embedded website
|
||||
/// - `false`: disable website serving
|
||||
/// - `"/path/to/website"`: serve custom website from path
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum WebsiteArg {
|
||||
Enabled(bool),
|
||||
Path(PathBuf),
|
||||
}
|
||||
|
||||
impl Default for WebsiteArg {
|
||||
fn default() -> Self {
|
||||
Self::Enabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for WebsiteArg {
|
||||
type Err = std::convert::Infallible;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match s.to_lowercase().as_str() {
|
||||
"true" | "1" | "yes" | "on" => Self::Enabled(true),
|
||||
"false" | "0" | "no" | "off" => Self::Enabled(false),
|
||||
_ => Self::Path(fix_user_path(s)),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,11 @@ homepage.workspace = true
|
||||
repository.workspace = true
|
||||
keywords = ["bitcoin", "blockchain", "analytics", "on-chain"]
|
||||
categories = ["api-bindings", "cryptography::cryptocurrencies"]
|
||||
exclude = ["examples/"]
|
||||
|
||||
[dependencies]
|
||||
brk_cohort = { workspace = true }
|
||||
brk_types = { workspace = true }
|
||||
minreq = { workspace = true }
|
||||
ureq = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -21,7 +21,7 @@ fn main() -> brk_client::Result<()> {
|
||||
|
||||
// Blockchain data (mempool.space compatible)
|
||||
let block = client.get_block_by_height(800000)?;
|
||||
let tx = client.get_tx("abc123...")?;
|
||||
let tx = client.get_tx("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d")?;
|
||||
let address = client.get_address("bc1q...")?;
|
||||
|
||||
// Metrics API - typed, chainable
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Basic example of using the BRK client.
|
||||
|
||||
use brk_client::{BrkClient, BrkClientOptions};
|
||||
use brk_types::{FormatResponse, Index, Metric};
|
||||
use brk_types::{FormatResponse, Index, RangeIndex};
|
||||
|
||||
fn main() -> brk_client::Result<()> {
|
||||
// Create client with default options
|
||||
@@ -14,59 +14,72 @@ fn main() -> brk_client::Result<()> {
|
||||
});
|
||||
|
||||
// Fetch price data using the typed metrics API
|
||||
// Using new idiomatic API: last(3).fetch()
|
||||
// day1() returns DateMetricEndpointBuilder, so fetch() returns DateMetricData
|
||||
let price_close = client
|
||||
.metrics()
|
||||
.price
|
||||
.usd
|
||||
.series()
|
||||
.prices
|
||||
.split
|
||||
.close
|
||||
.usd
|
||||
.by
|
||||
.dateindex()
|
||||
.day1()
|
||||
.last(3)
|
||||
.fetch()?;
|
||||
println!("Last 3 price close values: {:?}", price_close);
|
||||
println!("Last 3 price close values:");
|
||||
// iter_dates() returns Option (None for sub-daily indexes)
|
||||
for (date, value) in price_close.iter_dates().unwrap() {
|
||||
println!(" {}: {}", date, value);
|
||||
}
|
||||
// iter_timestamps() works for all date-based indexes including sub-daily
|
||||
for (ts, value) in price_close.iter_timestamps() {
|
||||
println!(" {}: {}", ts, value);
|
||||
}
|
||||
|
||||
// Fetch block data
|
||||
// Fetch block data with height index (non-date, returns MetricData)
|
||||
let block_count = client
|
||||
.metrics()
|
||||
.series()
|
||||
.blocks
|
||||
.count
|
||||
.block_count
|
||||
.total
|
||||
.sum
|
||||
._24h
|
||||
.by
|
||||
.dateindex()
|
||||
.day1()
|
||||
.last(3)
|
||||
.fetch()?;
|
||||
println!("Last 3 block count values: {:?}", block_count);
|
||||
println!("Last 3 block count values:");
|
||||
for (date, value) in block_count.iter_dates().unwrap() {
|
||||
println!(" {}: {}", date, value);
|
||||
}
|
||||
|
||||
// Fetch supply data
|
||||
dbg!(
|
||||
client
|
||||
.metrics()
|
||||
.supply
|
||||
.circulating
|
||||
.bitcoin
|
||||
.by
|
||||
.dateindex()
|
||||
.path()
|
||||
);
|
||||
// Fetch supply data as CSV
|
||||
dbg!(client.series().supply.circulating.btc.by.day1().path());
|
||||
let circulating = client
|
||||
.metrics()
|
||||
.series()
|
||||
.supply
|
||||
.circulating
|
||||
.bitcoin
|
||||
.btc
|
||||
.by
|
||||
.dateindex()
|
||||
.day1()
|
||||
.last(3)
|
||||
.fetch_csv()?;
|
||||
println!("Last 3 circulating supply values: {:?}", circulating);
|
||||
println!("Last 3 circulating supply (CSV): {:?}", circulating);
|
||||
|
||||
// Using generic metric fetching
|
||||
let metricdata = client.get_metric(
|
||||
Metric::from("price_close"),
|
||||
Index::DateIndex,
|
||||
Some(-3),
|
||||
// Using dynamic metric fetching with date_metric() for date-based indexes
|
||||
let date_metric = client
|
||||
.date_series_endpoint("price_close", Index::Day1)?
|
||||
.last(3)
|
||||
.fetch()?;
|
||||
println!("Dynamic date metric fetch:");
|
||||
for (date, value) in date_metric.iter_dates().unwrap() {
|
||||
println!(" {}: {}", date, value);
|
||||
}
|
||||
|
||||
// Using generic metric fetching (returns FormatResponse)
|
||||
let metricdata = client.get_series(
|
||||
"price_close".into(),
|
||||
Index::Day1,
|
||||
Some(RangeIndex::Int(-3)),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! and fetch data from each endpoint. Run with: cargo run --example tree
|
||||
|
||||
use brk_client::BrkClient;
|
||||
use brk_types::{Index, TreeNode};
|
||||
use brk_types::{Index, RangeIndex, TreeNode};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
/// A collected metric with its path and available indexes.
|
||||
@@ -45,7 +45,7 @@ fn main() -> brk_client::Result<()> {
|
||||
let client = BrkClient::new("http://localhost:3110");
|
||||
|
||||
// Get the metrics catalog tree
|
||||
let tree = client.get_metrics_tree()?;
|
||||
let tree = client.get_series_tree()?;
|
||||
|
||||
// Recursively collect all metrics
|
||||
let metrics = collect_metrics(&tree, "");
|
||||
@@ -55,14 +55,14 @@ fn main() -> brk_client::Result<()> {
|
||||
|
||||
for metric in &metrics {
|
||||
for index in &metric.indexes {
|
||||
let index_str = index.serialize_long();
|
||||
let index_str = index.name();
|
||||
let full_path = format!("{}.by.{}", metric.path, index_str);
|
||||
|
||||
match client.get_metric(
|
||||
match client.get_series(
|
||||
metric.name.as_str().into(),
|
||||
*index,
|
||||
None,
|
||||
Some(0),
|
||||
Some(RangeIndex::Int(0)),
|
||||
None,
|
||||
None,
|
||||
) {
|
||||
|
||||
+8137
-4172
File diff suppressed because it is too large
Load Diff
@@ -14,3 +14,6 @@ brk_traversable = { workspace = true }
|
||||
vecdb = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["vecdb"]
|
||||
|
||||
@@ -24,7 +24,7 @@ pub enum Filter {
|
||||
Term(Term), // STH/LTH
|
||||
Time(TimeFilter), // Age-based
|
||||
Amount(AmountFilter), // Value-based
|
||||
Epoch(HalvingEpoch), // Halving epoch
|
||||
Epoch(Halving), // Halving epoch
|
||||
Year(Year), // Calendar year
|
||||
Type(OutputType), // P2PKH, P2TR, etc.
|
||||
}
|
||||
@@ -48,5 +48,5 @@ ctx.full_name(&filter, "min_age_150d"); // "utxos_min_age_150d"
|
||||
## Built On
|
||||
|
||||
- `brk_error` for error handling
|
||||
- `brk_types` for `Sats`, `HalvingEpoch`, `OutputType`
|
||||
- `brk_types` for `Sats`, `Halving`, `OutputType`
|
||||
- `brk_traversable` for data structure traversal
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
use brk_traversable::Traversable;
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::Filter;
|
||||
|
||||
use super::{AmountRange, OverAmount, UnderAmount};
|
||||
|
||||
#[derive(Default, Clone, Traversable)]
|
||||
pub struct AddrGroups<T> {
|
||||
pub over_amount: OverAmount<T>,
|
||||
pub amount_range: AmountRange<T>,
|
||||
pub under_amount: UnderAmount<T>,
|
||||
}
|
||||
|
||||
impl<T> AddrGroups<T> {
|
||||
pub fn new<F>(mut create: F) -> Self
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> T,
|
||||
{
|
||||
Self {
|
||||
over_amount: OverAmount::new(&mut create),
|
||||
amount_range: AmountRange::new(&mut create),
|
||||
under_amount: UnderAmount::new(&mut create),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_new<F, E>(create: &F) -> Result<Self, E>
|
||||
where
|
||||
F: Fn(Filter, &'static str) -> Result<T, E>,
|
||||
{
|
||||
Ok(Self {
|
||||
over_amount: OverAmount::try_new(create)?,
|
||||
amount_range: AmountRange::try_new(create)?,
|
||||
under_amount: UnderAmount::try_new(create)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
self.over_amount
|
||||
.iter()
|
||||
.chain(self.amount_range.iter())
|
||||
.chain(self.under_amount.iter())
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
self.over_amount
|
||||
.iter_mut()
|
||||
.chain(self.amount_range.iter_mut())
|
||||
.chain(self.under_amount.iter_mut())
|
||||
}
|
||||
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
self.over_amount
|
||||
.par_iter_mut()
|
||||
.chain(self.amount_range.par_iter_mut())
|
||||
.chain(self.under_amount.par_iter_mut())
|
||||
}
|
||||
|
||||
pub fn iter_separate(&self) -> impl Iterator<Item = &T> {
|
||||
self.amount_range.iter()
|
||||
}
|
||||
|
||||
pub fn iter_separate_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
self.amount_range.iter_mut()
|
||||
}
|
||||
|
||||
pub fn par_iter_separate_mut(&mut self) -> impl ParallelIterator<Item = &mut T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
self.amount_range.par_iter_mut()
|
||||
}
|
||||
|
||||
pub fn iter_overlapping_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
self.under_amount
|
||||
.iter_mut()
|
||||
.chain(self.over_amount.iter_mut())
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
use brk_traversable::Traversable;
|
||||
use rayon::prelude::*;
|
||||
use vecdb::AnyExportableVec;
|
||||
|
||||
use crate::Filter;
|
||||
|
||||
use super::{ByAmountRange, ByGreatEqualAmount, ByLowerThanAmount};
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct AddressGroups<T> {
|
||||
pub ge_amount: ByGreatEqualAmount<T>,
|
||||
pub amount_range: ByAmountRange<T>,
|
||||
pub lt_amount: ByLowerThanAmount<T>,
|
||||
}
|
||||
|
||||
impl<T> AddressGroups<T> {
|
||||
pub fn new<F>(mut create: F) -> Self
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> T,
|
||||
{
|
||||
Self {
|
||||
ge_amount: ByGreatEqualAmount::new(&mut create),
|
||||
amount_range: ByAmountRange::new(&mut create),
|
||||
lt_amount: ByLowerThanAmount::new(&mut create),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_new<F, E>(create: &F) -> Result<Self, E>
|
||||
where
|
||||
F: Fn(Filter, &'static str) -> Result<T, E>,
|
||||
{
|
||||
Ok(Self {
|
||||
ge_amount: ByGreatEqualAmount::try_new(create)?,
|
||||
amount_range: ByAmountRange::try_new(create)?,
|
||||
lt_amount: ByLowerThanAmount::try_new(create)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
self.ge_amount
|
||||
.iter()
|
||||
.chain(self.amount_range.iter())
|
||||
.chain(self.lt_amount.iter())
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
self.ge_amount
|
||||
.iter_mut()
|
||||
.chain(self.amount_range.iter_mut())
|
||||
.chain(self.lt_amount.iter_mut())
|
||||
}
|
||||
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
self.ge_amount
|
||||
.par_iter_mut()
|
||||
.chain(self.amount_range.par_iter_mut())
|
||||
.chain(self.lt_amount.par_iter_mut())
|
||||
}
|
||||
|
||||
pub fn iter_separate(&self) -> impl Iterator<Item = &T> {
|
||||
self.amount_range.iter()
|
||||
}
|
||||
|
||||
pub fn iter_separate_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
self.amount_range.iter_mut()
|
||||
}
|
||||
|
||||
pub fn par_iter_separate_mut(&mut self) -> impl ParallelIterator<Item = &mut T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
self.amount_range.par_iter_mut()
|
||||
}
|
||||
|
||||
pub fn iter_overlapping_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
self.lt_amount.iter_mut().chain(self.ge_amount.iter_mut())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Traversable for AddressGroups<T>
|
||||
where
|
||||
ByGreatEqualAmount<T>: brk_traversable::Traversable,
|
||||
ByAmountRange<T>: brk_traversable::Traversable,
|
||||
ByLowerThanAmount<T>: brk_traversable::Traversable,
|
||||
T: Send + Sync,
|
||||
{
|
||||
fn to_tree_node(&self) -> brk_traversable::TreeNode {
|
||||
brk_traversable::TreeNode::Branch(
|
||||
[
|
||||
(String::from("ge_amount"), self.ge_amount.to_tree_node()),
|
||||
(
|
||||
String::from("amount_range"),
|
||||
self.amount_range.to_tree_node(),
|
||||
),
|
||||
(String::from("lt_amount"), self.lt_amount.to_tree_node()),
|
||||
]
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
fn iter_any_exportable(&self) -> impl Iterator<Item = &dyn AnyExportableVec> {
|
||||
[
|
||||
Box::new(self.ge_amount.iter_any_exportable())
|
||||
as Box<dyn Iterator<Item = &dyn AnyExportableVec>>,
|
||||
Box::new(self.amount_range.iter_any_exportable())
|
||||
as Box<dyn Iterator<Item = &dyn AnyExportableVec>>,
|
||||
Box::new(self.lt_amount.iter_any_exportable())
|
||||
as Box<dyn Iterator<Item = &dyn AnyExportableVec>>,
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::Age;
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use serde::Serialize;
|
||||
|
||||
use super::{CohortName, Filter, TimeFilter};
|
||||
|
||||
// Age boundary constants in hours
|
||||
pub const HOURS_1H: usize = 1;
|
||||
pub const HOURS_1D: usize = 24;
|
||||
pub const HOURS_1W: usize = 24 * 7;
|
||||
pub const HOURS_1M: usize = 24 * 30;
|
||||
pub const HOURS_2M: usize = 24 * 2 * 30;
|
||||
pub const HOURS_3M: usize = 24 * 3 * 30;
|
||||
pub const HOURS_4M: usize = 24 * 4 * 30;
|
||||
pub const HOURS_5M: usize = 24 * 5 * 30; // STH/LTH threshold
|
||||
pub const HOURS_6M: usize = 24 * 6 * 30;
|
||||
pub const HOURS_1Y: usize = 24 * 365;
|
||||
pub const HOURS_2Y: usize = 24 * 2 * 365;
|
||||
pub const HOURS_3Y: usize = 24 * 3 * 365;
|
||||
pub const HOURS_4Y: usize = 24 * 4 * 365;
|
||||
pub const HOURS_5Y: usize = 24 * 5 * 365;
|
||||
pub const HOURS_6Y: usize = 24 * 6 * 365;
|
||||
pub const HOURS_7Y: usize = 24 * 7 * 365;
|
||||
pub const HOURS_8Y: usize = 24 * 8 * 365;
|
||||
pub const HOURS_10Y: usize = 24 * 10 * 365;
|
||||
pub const HOURS_12Y: usize = 24 * 12 * 365;
|
||||
pub const HOURS_15Y: usize = 24 * 15 * 365;
|
||||
|
||||
/// Age boundaries in hours. Defines the cohort ranges:
|
||||
/// [0, 1h), [1h, 1d), [1d, 1w), [1w, 1m), ..., [15y, ∞)
|
||||
pub const AGE_BOUNDARIES: [usize; 20] = [
|
||||
HOURS_1H, HOURS_1D, HOURS_1W, HOURS_1M, HOURS_2M, HOURS_3M, HOURS_4M, HOURS_5M, HOURS_6M,
|
||||
HOURS_1Y, HOURS_2Y, HOURS_3Y, HOURS_4Y, HOURS_5Y, HOURS_6Y, HOURS_7Y, HOURS_8Y, HOURS_10Y,
|
||||
HOURS_12Y, HOURS_15Y,
|
||||
];
|
||||
|
||||
/// Age range bounds (end = usize::MAX means unbounded)
|
||||
pub const AGE_RANGE_BOUNDS: AgeRange<Range<usize>> = AgeRange {
|
||||
under_1h: 0..HOURS_1H,
|
||||
_1h_to_1d: HOURS_1H..HOURS_1D,
|
||||
_1d_to_1w: HOURS_1D..HOURS_1W,
|
||||
_1w_to_1m: HOURS_1W..HOURS_1M,
|
||||
_1m_to_2m: HOURS_1M..HOURS_2M,
|
||||
_2m_to_3m: HOURS_2M..HOURS_3M,
|
||||
_3m_to_4m: HOURS_3M..HOURS_4M,
|
||||
_4m_to_5m: HOURS_4M..HOURS_5M,
|
||||
_5m_to_6m: HOURS_5M..HOURS_6M,
|
||||
_6m_to_1y: HOURS_6M..HOURS_1Y,
|
||||
_1y_to_2y: HOURS_1Y..HOURS_2Y,
|
||||
_2y_to_3y: HOURS_2Y..HOURS_3Y,
|
||||
_3y_to_4y: HOURS_3Y..HOURS_4Y,
|
||||
_4y_to_5y: HOURS_4Y..HOURS_5Y,
|
||||
_5y_to_6y: HOURS_5Y..HOURS_6Y,
|
||||
_6y_to_7y: HOURS_6Y..HOURS_7Y,
|
||||
_7y_to_8y: HOURS_7Y..HOURS_8Y,
|
||||
_8y_to_10y: HOURS_8Y..HOURS_10Y,
|
||||
_10y_to_12y: HOURS_10Y..HOURS_12Y,
|
||||
_12y_to_15y: HOURS_12Y..HOURS_15Y,
|
||||
over_15y: HOURS_15Y..usize::MAX,
|
||||
};
|
||||
|
||||
/// Age range filters
|
||||
pub const AGE_RANGE_FILTERS: AgeRange<Filter> = AgeRange {
|
||||
under_1h: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS.under_1h)),
|
||||
_1h_to_1d: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._1h_to_1d)),
|
||||
_1d_to_1w: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._1d_to_1w)),
|
||||
_1w_to_1m: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._1w_to_1m)),
|
||||
_1m_to_2m: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._1m_to_2m)),
|
||||
_2m_to_3m: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._2m_to_3m)),
|
||||
_3m_to_4m: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._3m_to_4m)),
|
||||
_4m_to_5m: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._4m_to_5m)),
|
||||
_5m_to_6m: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._5m_to_6m)),
|
||||
_6m_to_1y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._6m_to_1y)),
|
||||
_1y_to_2y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._1y_to_2y)),
|
||||
_2y_to_3y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._2y_to_3y)),
|
||||
_3y_to_4y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._3y_to_4y)),
|
||||
_4y_to_5y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._4y_to_5y)),
|
||||
_5y_to_6y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._5y_to_6y)),
|
||||
_6y_to_7y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._6y_to_7y)),
|
||||
_7y_to_8y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._7y_to_8y)),
|
||||
_8y_to_10y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._8y_to_10y)),
|
||||
_10y_to_12y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._10y_to_12y)),
|
||||
_12y_to_15y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._12y_to_15y)),
|
||||
over_15y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS.over_15y)),
|
||||
};
|
||||
|
||||
/// Age range names
|
||||
pub const AGE_RANGE_NAMES: AgeRange<CohortName> = AgeRange {
|
||||
under_1h: CohortName::new("under_1h_old", "<1h", "Under 1 Hour Old"),
|
||||
_1h_to_1d: CohortName::new("1h_to_1d_old", "1h-1d", "1 Hour to 1 Day Old"),
|
||||
_1d_to_1w: CohortName::new("1d_to_1w_old", "1d-1w", "1 Day to 1 Week Old"),
|
||||
_1w_to_1m: CohortName::new("1w_to_1m_old", "1w-1m", "1 Week to 1 Month Old"),
|
||||
_1m_to_2m: CohortName::new("1m_to_2m_old", "1m-2m", "1 to 2 Months Old"),
|
||||
_2m_to_3m: CohortName::new("2m_to_3m_old", "2m-3m", "2 to 3 Months Old"),
|
||||
_3m_to_4m: CohortName::new("3m_to_4m_old", "3m-4m", "3 to 4 Months Old"),
|
||||
_4m_to_5m: CohortName::new("4m_to_5m_old", "4m-5m", "4 to 5 Months Old"),
|
||||
_5m_to_6m: CohortName::new("5m_to_6m_old", "5m-6m", "5 to 6 Months Old"),
|
||||
_6m_to_1y: CohortName::new("6m_to_1y_old", "6m-1y", "6 Months to 1 Year Old"),
|
||||
_1y_to_2y: CohortName::new("1y_to_2y_old", "1y-2y", "1 to 2 Years Old"),
|
||||
_2y_to_3y: CohortName::new("2y_to_3y_old", "2y-3y", "2 to 3 Years Old"),
|
||||
_3y_to_4y: CohortName::new("3y_to_4y_old", "3y-4y", "3 to 4 Years Old"),
|
||||
_4y_to_5y: CohortName::new("4y_to_5y_old", "4y-5y", "4 to 5 Years Old"),
|
||||
_5y_to_6y: CohortName::new("5y_to_6y_old", "5y-6y", "5 to 6 Years Old"),
|
||||
_6y_to_7y: CohortName::new("6y_to_7y_old", "6y-7y", "6 to 7 Years Old"),
|
||||
_7y_to_8y: CohortName::new("7y_to_8y_old", "7y-8y", "7 to 8 Years Old"),
|
||||
_8y_to_10y: CohortName::new("8y_to_10y_old", "8y-10y", "8 to 10 Years Old"),
|
||||
_10y_to_12y: CohortName::new("10y_to_12y_old", "10y-12y", "10 to 12 Years Old"),
|
||||
_12y_to_15y: CohortName::new("12y_to_15y_old", "12y-15y", "12 to 15 Years Old"),
|
||||
over_15y: CohortName::new("over_15y_old", "15y+", "15+ Years Old"),
|
||||
};
|
||||
|
||||
impl AgeRange<CohortName> {
|
||||
pub const fn names() -> &'static Self {
|
||||
&AGE_RANGE_NAMES
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Traversable, Serialize)]
|
||||
pub struct AgeRange<T> {
|
||||
pub under_1h: T,
|
||||
pub _1h_to_1d: T,
|
||||
pub _1d_to_1w: T,
|
||||
pub _1w_to_1m: T,
|
||||
pub _1m_to_2m: T,
|
||||
pub _2m_to_3m: T,
|
||||
pub _3m_to_4m: T,
|
||||
pub _4m_to_5m: T,
|
||||
pub _5m_to_6m: T,
|
||||
pub _6m_to_1y: T,
|
||||
pub _1y_to_2y: T,
|
||||
pub _2y_to_3y: T,
|
||||
pub _3y_to_4y: T,
|
||||
pub _4y_to_5y: T,
|
||||
pub _5y_to_6y: T,
|
||||
pub _6y_to_7y: T,
|
||||
pub _7y_to_8y: T,
|
||||
pub _8y_to_10y: T,
|
||||
pub _10y_to_12y: T,
|
||||
pub _12y_to_15y: T,
|
||||
pub over_15y: T,
|
||||
}
|
||||
|
||||
impl<T> AgeRange<T> {
|
||||
/// Get mutable reference by Age. O(1).
|
||||
#[inline]
|
||||
pub fn get_mut(&mut self, age: Age) -> &mut T {
|
||||
match age.hours() {
|
||||
0..HOURS_1H => &mut self.under_1h,
|
||||
HOURS_1H..HOURS_1D => &mut self._1h_to_1d,
|
||||
HOURS_1D..HOURS_1W => &mut self._1d_to_1w,
|
||||
HOURS_1W..HOURS_1M => &mut self._1w_to_1m,
|
||||
HOURS_1M..HOURS_2M => &mut self._1m_to_2m,
|
||||
HOURS_2M..HOURS_3M => &mut self._2m_to_3m,
|
||||
HOURS_3M..HOURS_4M => &mut self._3m_to_4m,
|
||||
HOURS_4M..HOURS_5M => &mut self._4m_to_5m,
|
||||
HOURS_5M..HOURS_6M => &mut self._5m_to_6m,
|
||||
HOURS_6M..HOURS_1Y => &mut self._6m_to_1y,
|
||||
HOURS_1Y..HOURS_2Y => &mut self._1y_to_2y,
|
||||
HOURS_2Y..HOURS_3Y => &mut self._2y_to_3y,
|
||||
HOURS_3Y..HOURS_4Y => &mut self._3y_to_4y,
|
||||
HOURS_4Y..HOURS_5Y => &mut self._4y_to_5y,
|
||||
HOURS_5Y..HOURS_6Y => &mut self._5y_to_6y,
|
||||
HOURS_6Y..HOURS_7Y => &mut self._6y_to_7y,
|
||||
HOURS_7Y..HOURS_8Y => &mut self._7y_to_8y,
|
||||
HOURS_8Y..HOURS_10Y => &mut self._8y_to_10y,
|
||||
HOURS_10Y..HOURS_12Y => &mut self._10y_to_12y,
|
||||
HOURS_12Y..HOURS_15Y => &mut self._12y_to_15y,
|
||||
_ => &mut self.over_15y,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get reference by Age. O(1).
|
||||
#[inline]
|
||||
pub fn get(&self, age: Age) -> &T {
|
||||
match age.hours() {
|
||||
0..HOURS_1H => &self.under_1h,
|
||||
HOURS_1H..HOURS_1D => &self._1h_to_1d,
|
||||
HOURS_1D..HOURS_1W => &self._1d_to_1w,
|
||||
HOURS_1W..HOURS_1M => &self._1w_to_1m,
|
||||
HOURS_1M..HOURS_2M => &self._1m_to_2m,
|
||||
HOURS_2M..HOURS_3M => &self._2m_to_3m,
|
||||
HOURS_3M..HOURS_4M => &self._3m_to_4m,
|
||||
HOURS_4M..HOURS_5M => &self._4m_to_5m,
|
||||
HOURS_5M..HOURS_6M => &self._5m_to_6m,
|
||||
HOURS_6M..HOURS_1Y => &self._6m_to_1y,
|
||||
HOURS_1Y..HOURS_2Y => &self._1y_to_2y,
|
||||
HOURS_2Y..HOURS_3Y => &self._2y_to_3y,
|
||||
HOURS_3Y..HOURS_4Y => &self._3y_to_4y,
|
||||
HOURS_4Y..HOURS_5Y => &self._4y_to_5y,
|
||||
HOURS_5Y..HOURS_6Y => &self._5y_to_6y,
|
||||
HOURS_6Y..HOURS_7Y => &self._6y_to_7y,
|
||||
HOURS_7Y..HOURS_8Y => &self._7y_to_8y,
|
||||
HOURS_8Y..HOURS_10Y => &self._8y_to_10y,
|
||||
HOURS_10Y..HOURS_12Y => &self._10y_to_12y,
|
||||
HOURS_12Y..HOURS_15Y => &self._12y_to_15y,
|
||||
_ => &self.over_15y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_array(arr: [T; 21]) -> Self {
|
||||
let [
|
||||
a0,
|
||||
a1,
|
||||
a2,
|
||||
a3,
|
||||
a4,
|
||||
a5,
|
||||
a6,
|
||||
a7,
|
||||
a8,
|
||||
a9,
|
||||
a10,
|
||||
a11,
|
||||
a12,
|
||||
a13,
|
||||
a14,
|
||||
a15,
|
||||
a16,
|
||||
a17,
|
||||
a18,
|
||||
a19,
|
||||
a20,
|
||||
] = arr;
|
||||
Self {
|
||||
under_1h: a0,
|
||||
_1h_to_1d: a1,
|
||||
_1d_to_1w: a2,
|
||||
_1w_to_1m: a3,
|
||||
_1m_to_2m: a4,
|
||||
_2m_to_3m: a5,
|
||||
_3m_to_4m: a6,
|
||||
_4m_to_5m: a7,
|
||||
_5m_to_6m: a8,
|
||||
_6m_to_1y: a9,
|
||||
_1y_to_2y: a10,
|
||||
_2y_to_3y: a11,
|
||||
_3y_to_4y: a12,
|
||||
_4y_to_5y: a13,
|
||||
_5y_to_6y: a14,
|
||||
_6y_to_7y: a15,
|
||||
_7y_to_8y: a16,
|
||||
_8y_to_10y: a17,
|
||||
_10y_to_12y: a18,
|
||||
_12y_to_15y: a19,
|
||||
over_15y: a20,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new<F>(mut create: F) -> Self
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> T,
|
||||
{
|
||||
let f = AGE_RANGE_FILTERS;
|
||||
let n = AGE_RANGE_NAMES;
|
||||
Self {
|
||||
under_1h: create(f.under_1h.clone(), n.under_1h.id),
|
||||
_1h_to_1d: create(f._1h_to_1d.clone(), n._1h_to_1d.id),
|
||||
_1d_to_1w: create(f._1d_to_1w.clone(), n._1d_to_1w.id),
|
||||
_1w_to_1m: create(f._1w_to_1m.clone(), n._1w_to_1m.id),
|
||||
_1m_to_2m: create(f._1m_to_2m.clone(), n._1m_to_2m.id),
|
||||
_2m_to_3m: create(f._2m_to_3m.clone(), n._2m_to_3m.id),
|
||||
_3m_to_4m: create(f._3m_to_4m.clone(), n._3m_to_4m.id),
|
||||
_4m_to_5m: create(f._4m_to_5m.clone(), n._4m_to_5m.id),
|
||||
_5m_to_6m: create(f._5m_to_6m.clone(), n._5m_to_6m.id),
|
||||
_6m_to_1y: create(f._6m_to_1y.clone(), n._6m_to_1y.id),
|
||||
_1y_to_2y: create(f._1y_to_2y.clone(), n._1y_to_2y.id),
|
||||
_2y_to_3y: create(f._2y_to_3y.clone(), n._2y_to_3y.id),
|
||||
_3y_to_4y: create(f._3y_to_4y.clone(), n._3y_to_4y.id),
|
||||
_4y_to_5y: create(f._4y_to_5y.clone(), n._4y_to_5y.id),
|
||||
_5y_to_6y: create(f._5y_to_6y.clone(), n._5y_to_6y.id),
|
||||
_6y_to_7y: create(f._6y_to_7y.clone(), n._6y_to_7y.id),
|
||||
_7y_to_8y: create(f._7y_to_8y.clone(), n._7y_to_8y.id),
|
||||
_8y_to_10y: create(f._8y_to_10y.clone(), n._8y_to_10y.id),
|
||||
_10y_to_12y: create(f._10y_to_12y.clone(), n._10y_to_12y.id),
|
||||
_12y_to_15y: create(f._12y_to_15y.clone(), n._12y_to_15y.id),
|
||||
over_15y: create(f.over_15y.clone(), n.over_15y.id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_new<F, E>(mut create: F) -> Result<Self, E>
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> Result<T, E>,
|
||||
{
|
||||
let f = AGE_RANGE_FILTERS;
|
||||
let n = AGE_RANGE_NAMES;
|
||||
Ok(Self {
|
||||
under_1h: create(f.under_1h.clone(), n.under_1h.id)?,
|
||||
_1h_to_1d: create(f._1h_to_1d.clone(), n._1h_to_1d.id)?,
|
||||
_1d_to_1w: create(f._1d_to_1w.clone(), n._1d_to_1w.id)?,
|
||||
_1w_to_1m: create(f._1w_to_1m.clone(), n._1w_to_1m.id)?,
|
||||
_1m_to_2m: create(f._1m_to_2m.clone(), n._1m_to_2m.id)?,
|
||||
_2m_to_3m: create(f._2m_to_3m.clone(), n._2m_to_3m.id)?,
|
||||
_3m_to_4m: create(f._3m_to_4m.clone(), n._3m_to_4m.id)?,
|
||||
_4m_to_5m: create(f._4m_to_5m.clone(), n._4m_to_5m.id)?,
|
||||
_5m_to_6m: create(f._5m_to_6m.clone(), n._5m_to_6m.id)?,
|
||||
_6m_to_1y: create(f._6m_to_1y.clone(), n._6m_to_1y.id)?,
|
||||
_1y_to_2y: create(f._1y_to_2y.clone(), n._1y_to_2y.id)?,
|
||||
_2y_to_3y: create(f._2y_to_3y.clone(), n._2y_to_3y.id)?,
|
||||
_3y_to_4y: create(f._3y_to_4y.clone(), n._3y_to_4y.id)?,
|
||||
_4y_to_5y: create(f._4y_to_5y.clone(), n._4y_to_5y.id)?,
|
||||
_5y_to_6y: create(f._5y_to_6y.clone(), n._5y_to_6y.id)?,
|
||||
_6y_to_7y: create(f._6y_to_7y.clone(), n._6y_to_7y.id)?,
|
||||
_7y_to_8y: create(f._7y_to_8y.clone(), n._7y_to_8y.id)?,
|
||||
_8y_to_10y: create(f._8y_to_10y.clone(), n._8y_to_10y.id)?,
|
||||
_10y_to_12y: create(f._10y_to_12y.clone(), n._10y_to_12y.id)?,
|
||||
_12y_to_15y: create(f._12y_to_15y.clone(), n._12y_to_15y.id)?,
|
||||
over_15y: create(f.over_15y.clone(), n.over_15y.id)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
[
|
||||
&self.under_1h,
|
||||
&self._1h_to_1d,
|
||||
&self._1d_to_1w,
|
||||
&self._1w_to_1m,
|
||||
&self._1m_to_2m,
|
||||
&self._2m_to_3m,
|
||||
&self._3m_to_4m,
|
||||
&self._4m_to_5m,
|
||||
&self._5m_to_6m,
|
||||
&self._6m_to_1y,
|
||||
&self._1y_to_2y,
|
||||
&self._2y_to_3y,
|
||||
&self._3y_to_4y,
|
||||
&self._4y_to_5y,
|
||||
&self._5y_to_6y,
|
||||
&self._6y_to_7y,
|
||||
&self._7y_to_8y,
|
||||
&self._8y_to_10y,
|
||||
&self._10y_to_12y,
|
||||
&self._12y_to_15y,
|
||||
&self.over_15y,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
[
|
||||
&mut self.under_1h,
|
||||
&mut self._1h_to_1d,
|
||||
&mut self._1d_to_1w,
|
||||
&mut self._1w_to_1m,
|
||||
&mut self._1m_to_2m,
|
||||
&mut self._2m_to_3m,
|
||||
&mut self._3m_to_4m,
|
||||
&mut self._4m_to_5m,
|
||||
&mut self._5m_to_6m,
|
||||
&mut self._6m_to_1y,
|
||||
&mut self._1y_to_2y,
|
||||
&mut self._2y_to_3y,
|
||||
&mut self._3y_to_4y,
|
||||
&mut self._4y_to_5y,
|
||||
&mut self._5y_to_6y,
|
||||
&mut self._6y_to_7y,
|
||||
&mut self._7y_to_8y,
|
||||
&mut self._8y_to_10y,
|
||||
&mut self._10y_to_12y,
|
||||
&mut self._12y_to_15y,
|
||||
&mut self.over_15y,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
[
|
||||
&mut self.under_1h,
|
||||
&mut self._1h_to_1d,
|
||||
&mut self._1d_to_1w,
|
||||
&mut self._1w_to_1m,
|
||||
&mut self._1m_to_2m,
|
||||
&mut self._2m_to_3m,
|
||||
&mut self._3m_to_4m,
|
||||
&mut self._4m_to_5m,
|
||||
&mut self._5m_to_6m,
|
||||
&mut self._6m_to_1y,
|
||||
&mut self._1y_to_2y,
|
||||
&mut self._2y_to_3y,
|
||||
&mut self._3y_to_4y,
|
||||
&mut self._4y_to_5y,
|
||||
&mut self._5y_to_6y,
|
||||
&mut self._6y_to_7y,
|
||||
&mut self._7y_to_8y,
|
||||
&mut self._8y_to_10y,
|
||||
&mut self._10y_to_12y,
|
||||
&mut self._12y_to_15y,
|
||||
&mut self.over_15y,
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
use std::ops::{Add, AddAssign, Range};
|
||||
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::Sats;
|
||||
use rayon::prelude::*;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::{AmountFilter, CohortName, Filter};
|
||||
|
||||
/// Bucket index for amount ranges. Use for cheap comparisons and direct lookups.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct AmountBucket(u8);
|
||||
|
||||
impl AmountBucket {
|
||||
/// Returns (self, other) if buckets differ, None if same.
|
||||
/// Use with `AmountRange::get_mut_by_bucket` to avoid recomputing.
|
||||
#[inline(always)]
|
||||
pub fn transition_to(self, other: Self) -> Option<(Self, Self)> {
|
||||
if self != other {
|
||||
Some((self, other))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn index(self) -> u8 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Sats> for AmountBucket {
|
||||
#[inline(always)]
|
||||
fn from(value: Sats) -> Self {
|
||||
Self(match value {
|
||||
v if v < Sats::_1 => 0,
|
||||
v if v < Sats::_10 => 1,
|
||||
v if v < Sats::_100 => 2,
|
||||
v if v < Sats::_1K => 3,
|
||||
v if v < Sats::_10K => 4,
|
||||
v if v < Sats::_100K => 5,
|
||||
v if v < Sats::_1M => 6,
|
||||
v if v < Sats::_10M => 7,
|
||||
v if v < Sats::_1BTC => 8,
|
||||
v if v < Sats::_10BTC => 9,
|
||||
v if v < Sats::_100BTC => 10,
|
||||
v if v < Sats::_1K_BTC => 11,
|
||||
v if v < Sats::_10K_BTC => 12,
|
||||
v if v < Sats::_100K_BTC => 13,
|
||||
_ => 14,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if two amounts are in different buckets. O(1).
|
||||
#[inline(always)]
|
||||
pub fn amounts_in_different_buckets(a: Sats, b: Sats) -> bool {
|
||||
AmountBucket::from(a) != AmountBucket::from(b)
|
||||
}
|
||||
|
||||
/// Amount range bounds
|
||||
pub const AMOUNT_RANGE_BOUNDS: AmountRange<Range<Sats>> = AmountRange {
|
||||
_0sats: Sats::ZERO..Sats::_1,
|
||||
_1sat_to_10sats: Sats::_1..Sats::_10,
|
||||
_10sats_to_100sats: Sats::_10..Sats::_100,
|
||||
_100sats_to_1k_sats: Sats::_100..Sats::_1K,
|
||||
_1k_sats_to_10k_sats: Sats::_1K..Sats::_10K,
|
||||
_10k_sats_to_100k_sats: Sats::_10K..Sats::_100K,
|
||||
_100k_sats_to_1m_sats: Sats::_100K..Sats::_1M,
|
||||
_1m_sats_to_10m_sats: Sats::_1M..Sats::_10M,
|
||||
_10m_sats_to_1btc: Sats::_10M..Sats::_1BTC,
|
||||
_1btc_to_10btc: Sats::_1BTC..Sats::_10BTC,
|
||||
_10btc_to_100btc: Sats::_10BTC..Sats::_100BTC,
|
||||
_100btc_to_1k_btc: Sats::_100BTC..Sats::_1K_BTC,
|
||||
_1k_btc_to_10k_btc: Sats::_1K_BTC..Sats::_10K_BTC,
|
||||
_10k_btc_to_100k_btc: Sats::_10K_BTC..Sats::_100K_BTC,
|
||||
over_100k_btc: Sats::_100K_BTC..Sats::MAX,
|
||||
};
|
||||
|
||||
/// Amount range names
|
||||
pub const AMOUNT_RANGE_NAMES: AmountRange<CohortName> = AmountRange {
|
||||
_0sats: CohortName::new("0sats", "0 sats", "0 Sats"),
|
||||
_1sat_to_10sats: CohortName::new("1sat_to_10sats", "1-10 sats", "1-10 Sats"),
|
||||
_10sats_to_100sats: CohortName::new("10sats_to_100sats", "10-100 sats", "10-100 Sats"),
|
||||
_100sats_to_1k_sats: CohortName::new("100sats_to_1k_sats", "100-1k sats", "100-1K Sats"),
|
||||
_1k_sats_to_10k_sats: CohortName::new("1k_sats_to_10k_sats", "1k-10k sats", "1K-10K Sats"),
|
||||
_10k_sats_to_100k_sats: CohortName::new(
|
||||
"10k_sats_to_100k_sats",
|
||||
"10k-100k sats",
|
||||
"10K-100K Sats",
|
||||
),
|
||||
_100k_sats_to_1m_sats: CohortName::new("100k_sats_to_1m_sats", "100k-1M sats", "100K-1M Sats"),
|
||||
_1m_sats_to_10m_sats: CohortName::new("1m_sats_to_10m_sats", "1M-10M sats", "1M-10M Sats"),
|
||||
_10m_sats_to_1btc: CohortName::new("10m_sats_to_1btc", "0.1-1 BTC", "0.1-1 BTC"),
|
||||
_1btc_to_10btc: CohortName::new("1btc_to_10btc", "1-10 BTC", "1-10 BTC"),
|
||||
_10btc_to_100btc: CohortName::new("10btc_to_100btc", "10-100 BTC", "10-100 BTC"),
|
||||
_100btc_to_1k_btc: CohortName::new("100btc_to_1k_btc", "100-1k BTC", "100-1K BTC"),
|
||||
_1k_btc_to_10k_btc: CohortName::new("1k_btc_to_10k_btc", "1k-10k BTC", "1K-10K BTC"),
|
||||
_10k_btc_to_100k_btc: CohortName::new("10k_btc_to_100k_btc", "10k-100k BTC", "10K-100K BTC"),
|
||||
over_100k_btc: CohortName::new("over_100k_btc", "100k+ BTC", "100K+ BTC"),
|
||||
};
|
||||
|
||||
/// Amount range filters
|
||||
pub const AMOUNT_RANGE_FILTERS: AmountRange<Filter> = AmountRange {
|
||||
_0sats: Filter::Amount(AmountFilter::Range(AMOUNT_RANGE_BOUNDS._0sats)),
|
||||
_1sat_to_10sats: Filter::Amount(AmountFilter::Range(AMOUNT_RANGE_BOUNDS._1sat_to_10sats)),
|
||||
_10sats_to_100sats: Filter::Amount(AmountFilter::Range(AMOUNT_RANGE_BOUNDS._10sats_to_100sats)),
|
||||
_100sats_to_1k_sats: Filter::Amount(AmountFilter::Range(
|
||||
AMOUNT_RANGE_BOUNDS._100sats_to_1k_sats,
|
||||
)),
|
||||
_1k_sats_to_10k_sats: Filter::Amount(AmountFilter::Range(
|
||||
AMOUNT_RANGE_BOUNDS._1k_sats_to_10k_sats,
|
||||
)),
|
||||
_10k_sats_to_100k_sats: Filter::Amount(AmountFilter::Range(
|
||||
AMOUNT_RANGE_BOUNDS._10k_sats_to_100k_sats,
|
||||
)),
|
||||
_100k_sats_to_1m_sats: Filter::Amount(AmountFilter::Range(
|
||||
AMOUNT_RANGE_BOUNDS._100k_sats_to_1m_sats,
|
||||
)),
|
||||
_1m_sats_to_10m_sats: Filter::Amount(AmountFilter::Range(
|
||||
AMOUNT_RANGE_BOUNDS._1m_sats_to_10m_sats,
|
||||
)),
|
||||
_10m_sats_to_1btc: Filter::Amount(AmountFilter::Range(AMOUNT_RANGE_BOUNDS._10m_sats_to_1btc)),
|
||||
_1btc_to_10btc: Filter::Amount(AmountFilter::Range(AMOUNT_RANGE_BOUNDS._1btc_to_10btc)),
|
||||
_10btc_to_100btc: Filter::Amount(AmountFilter::Range(AMOUNT_RANGE_BOUNDS._10btc_to_100btc)),
|
||||
_100btc_to_1k_btc: Filter::Amount(AmountFilter::Range(AMOUNT_RANGE_BOUNDS._100btc_to_1k_btc)),
|
||||
_1k_btc_to_10k_btc: Filter::Amount(AmountFilter::Range(AMOUNT_RANGE_BOUNDS._1k_btc_to_10k_btc)),
|
||||
_10k_btc_to_100k_btc: Filter::Amount(AmountFilter::Range(
|
||||
AMOUNT_RANGE_BOUNDS._10k_btc_to_100k_btc,
|
||||
)),
|
||||
over_100k_btc: Filter::Amount(AmountFilter::Range(AMOUNT_RANGE_BOUNDS.over_100k_btc)),
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Clone, Traversable, Serialize)]
|
||||
pub struct AmountRange<T> {
|
||||
pub _0sats: T,
|
||||
pub _1sat_to_10sats: T,
|
||||
pub _10sats_to_100sats: T,
|
||||
pub _100sats_to_1k_sats: T,
|
||||
pub _1k_sats_to_10k_sats: T,
|
||||
pub _10k_sats_to_100k_sats: T,
|
||||
pub _100k_sats_to_1m_sats: T,
|
||||
pub _1m_sats_to_10m_sats: T,
|
||||
pub _10m_sats_to_1btc: T,
|
||||
pub _1btc_to_10btc: T,
|
||||
pub _10btc_to_100btc: T,
|
||||
pub _100btc_to_1k_btc: T,
|
||||
pub _1k_btc_to_10k_btc: T,
|
||||
pub _10k_btc_to_100k_btc: T,
|
||||
pub over_100k_btc: T,
|
||||
}
|
||||
|
||||
impl AmountRange<CohortName> {
|
||||
pub const fn names() -> &'static Self {
|
||||
&AMOUNT_RANGE_NAMES
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AmountRange<T> {
|
||||
pub fn new<F>(mut create: F) -> Self
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> T,
|
||||
{
|
||||
let f = AMOUNT_RANGE_FILTERS;
|
||||
let n = AMOUNT_RANGE_NAMES;
|
||||
Self {
|
||||
_0sats: create(f._0sats.clone(), n._0sats.id),
|
||||
_1sat_to_10sats: create(f._1sat_to_10sats.clone(), n._1sat_to_10sats.id),
|
||||
_10sats_to_100sats: create(f._10sats_to_100sats.clone(), n._10sats_to_100sats.id),
|
||||
_100sats_to_1k_sats: create(f._100sats_to_1k_sats.clone(), n._100sats_to_1k_sats.id),
|
||||
_1k_sats_to_10k_sats: create(f._1k_sats_to_10k_sats.clone(), n._1k_sats_to_10k_sats.id),
|
||||
_10k_sats_to_100k_sats: create(
|
||||
f._10k_sats_to_100k_sats.clone(),
|
||||
n._10k_sats_to_100k_sats.id,
|
||||
),
|
||||
_100k_sats_to_1m_sats: create(
|
||||
f._100k_sats_to_1m_sats.clone(),
|
||||
n._100k_sats_to_1m_sats.id,
|
||||
),
|
||||
_1m_sats_to_10m_sats: create(f._1m_sats_to_10m_sats.clone(), n._1m_sats_to_10m_sats.id),
|
||||
_10m_sats_to_1btc: create(f._10m_sats_to_1btc.clone(), n._10m_sats_to_1btc.id),
|
||||
_1btc_to_10btc: create(f._1btc_to_10btc.clone(), n._1btc_to_10btc.id),
|
||||
_10btc_to_100btc: create(f._10btc_to_100btc.clone(), n._10btc_to_100btc.id),
|
||||
_100btc_to_1k_btc: create(f._100btc_to_1k_btc.clone(), n._100btc_to_1k_btc.id),
|
||||
_1k_btc_to_10k_btc: create(f._1k_btc_to_10k_btc.clone(), n._1k_btc_to_10k_btc.id),
|
||||
_10k_btc_to_100k_btc: create(f._10k_btc_to_100k_btc.clone(), n._10k_btc_to_100k_btc.id),
|
||||
over_100k_btc: create(f.over_100k_btc.clone(), n.over_100k_btc.id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_new<F, E>(mut create: F) -> Result<Self, E>
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> Result<T, E>,
|
||||
{
|
||||
let f = AMOUNT_RANGE_FILTERS;
|
||||
let n = AMOUNT_RANGE_NAMES;
|
||||
Ok(Self {
|
||||
_0sats: create(f._0sats.clone(), n._0sats.id)?,
|
||||
_1sat_to_10sats: create(f._1sat_to_10sats.clone(), n._1sat_to_10sats.id)?,
|
||||
_10sats_to_100sats: create(f._10sats_to_100sats.clone(), n._10sats_to_100sats.id)?,
|
||||
_100sats_to_1k_sats: create(f._100sats_to_1k_sats.clone(), n._100sats_to_1k_sats.id)?,
|
||||
_1k_sats_to_10k_sats: create(
|
||||
f._1k_sats_to_10k_sats.clone(),
|
||||
n._1k_sats_to_10k_sats.id,
|
||||
)?,
|
||||
_10k_sats_to_100k_sats: create(
|
||||
f._10k_sats_to_100k_sats.clone(),
|
||||
n._10k_sats_to_100k_sats.id,
|
||||
)?,
|
||||
_100k_sats_to_1m_sats: create(
|
||||
f._100k_sats_to_1m_sats.clone(),
|
||||
n._100k_sats_to_1m_sats.id,
|
||||
)?,
|
||||
_1m_sats_to_10m_sats: create(
|
||||
f._1m_sats_to_10m_sats.clone(),
|
||||
n._1m_sats_to_10m_sats.id,
|
||||
)?,
|
||||
_10m_sats_to_1btc: create(f._10m_sats_to_1btc.clone(), n._10m_sats_to_1btc.id)?,
|
||||
_1btc_to_10btc: create(f._1btc_to_10btc.clone(), n._1btc_to_10btc.id)?,
|
||||
_10btc_to_100btc: create(f._10btc_to_100btc.clone(), n._10btc_to_100btc.id)?,
|
||||
_100btc_to_1k_btc: create(f._100btc_to_1k_btc.clone(), n._100btc_to_1k_btc.id)?,
|
||||
_1k_btc_to_10k_btc: create(f._1k_btc_to_10k_btc.clone(), n._1k_btc_to_10k_btc.id)?,
|
||||
_10k_btc_to_100k_btc: create(
|
||||
f._10k_btc_to_100k_btc.clone(),
|
||||
n._10k_btc_to_100k_btc.id,
|
||||
)?,
|
||||
over_100k_btc: create(f.over_100k_btc.clone(), n.over_100k_btc.id)?,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get(&self, value: Sats) -> &T {
|
||||
match AmountBucket::from(value).0 {
|
||||
0 => &self._0sats,
|
||||
1 => &self._1sat_to_10sats,
|
||||
2 => &self._10sats_to_100sats,
|
||||
3 => &self._100sats_to_1k_sats,
|
||||
4 => &self._1k_sats_to_10k_sats,
|
||||
5 => &self._10k_sats_to_100k_sats,
|
||||
6 => &self._100k_sats_to_1m_sats,
|
||||
7 => &self._1m_sats_to_10m_sats,
|
||||
8 => &self._10m_sats_to_1btc,
|
||||
9 => &self._1btc_to_10btc,
|
||||
10 => &self._10btc_to_100btc,
|
||||
11 => &self._100btc_to_1k_btc,
|
||||
12 => &self._1k_btc_to_10k_btc,
|
||||
13 => &self._10k_btc_to_100k_btc,
|
||||
_ => &self.over_100k_btc,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get_mut(&mut self, value: Sats) -> &mut T {
|
||||
self.get_mut_by_bucket(AmountBucket::from(value))
|
||||
}
|
||||
|
||||
/// Get mutable reference by pre-computed bucket index.
|
||||
/// Use with `AmountBucket::transition_to` to avoid recomputing bucket.
|
||||
#[inline(always)]
|
||||
pub fn get_mut_by_bucket(&mut self, bucket: AmountBucket) -> &mut T {
|
||||
match bucket.0 {
|
||||
0 => &mut self._0sats,
|
||||
1 => &mut self._1sat_to_10sats,
|
||||
2 => &mut self._10sats_to_100sats,
|
||||
3 => &mut self._100sats_to_1k_sats,
|
||||
4 => &mut self._1k_sats_to_10k_sats,
|
||||
5 => &mut self._10k_sats_to_100k_sats,
|
||||
6 => &mut self._100k_sats_to_1m_sats,
|
||||
7 => &mut self._1m_sats_to_10m_sats,
|
||||
8 => &mut self._10m_sats_to_1btc,
|
||||
9 => &mut self._1btc_to_10btc,
|
||||
10 => &mut self._10btc_to_100btc,
|
||||
11 => &mut self._100btc_to_1k_btc,
|
||||
12 => &mut self._1k_btc_to_10k_btc,
|
||||
13 => &mut self._10k_btc_to_100k_btc,
|
||||
_ => &mut self.over_100k_btc,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
[
|
||||
&self._0sats,
|
||||
&self._1sat_to_10sats,
|
||||
&self._10sats_to_100sats,
|
||||
&self._100sats_to_1k_sats,
|
||||
&self._1k_sats_to_10k_sats,
|
||||
&self._10k_sats_to_100k_sats,
|
||||
&self._100k_sats_to_1m_sats,
|
||||
&self._1m_sats_to_10m_sats,
|
||||
&self._10m_sats_to_1btc,
|
||||
&self._1btc_to_10btc,
|
||||
&self._10btc_to_100btc,
|
||||
&self._100btc_to_1k_btc,
|
||||
&self._1k_btc_to_10k_btc,
|
||||
&self._10k_btc_to_100k_btc,
|
||||
&self.over_100k_btc,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn iter_typed(&self) -> impl Iterator<Item = (Sats, &T)> {
|
||||
[
|
||||
(Sats::ZERO, &self._0sats),
|
||||
(Sats::_1, &self._1sat_to_10sats),
|
||||
(Sats::_10, &self._10sats_to_100sats),
|
||||
(Sats::_100, &self._100sats_to_1k_sats),
|
||||
(Sats::_1K, &self._1k_sats_to_10k_sats),
|
||||
(Sats::_10K, &self._10k_sats_to_100k_sats),
|
||||
(Sats::_100K, &self._100k_sats_to_1m_sats),
|
||||
(Sats::_1M, &self._1m_sats_to_10m_sats),
|
||||
(Sats::_10M, &self._10m_sats_to_1btc),
|
||||
(Sats::_1BTC, &self._1btc_to_10btc),
|
||||
(Sats::_10BTC, &self._10btc_to_100btc),
|
||||
(Sats::_100BTC, &self._100btc_to_1k_btc),
|
||||
(Sats::_1K_BTC, &self._1k_btc_to_10k_btc),
|
||||
(Sats::_10K_BTC, &self._10k_btc_to_100k_btc),
|
||||
(Sats::_100K_BTC, &self.over_100k_btc),
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
[
|
||||
&mut self._0sats,
|
||||
&mut self._1sat_to_10sats,
|
||||
&mut self._10sats_to_100sats,
|
||||
&mut self._100sats_to_1k_sats,
|
||||
&mut self._1k_sats_to_10k_sats,
|
||||
&mut self._10k_sats_to_100k_sats,
|
||||
&mut self._100k_sats_to_1m_sats,
|
||||
&mut self._1m_sats_to_10m_sats,
|
||||
&mut self._10m_sats_to_1btc,
|
||||
&mut self._1btc_to_10btc,
|
||||
&mut self._10btc_to_100btc,
|
||||
&mut self._100btc_to_1k_btc,
|
||||
&mut self._1k_btc_to_10k_btc,
|
||||
&mut self._10k_btc_to_100k_btc,
|
||||
&mut self.over_100k_btc,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
[
|
||||
&mut self._0sats,
|
||||
&mut self._1sat_to_10sats,
|
||||
&mut self._10sats_to_100sats,
|
||||
&mut self._100sats_to_1k_sats,
|
||||
&mut self._1k_sats_to_10k_sats,
|
||||
&mut self._10k_sats_to_100k_sats,
|
||||
&mut self._100k_sats_to_1m_sats,
|
||||
&mut self._1m_sats_to_10m_sats,
|
||||
&mut self._10m_sats_to_1btc,
|
||||
&mut self._1btc_to_10btc,
|
||||
&mut self._10btc_to_100btc,
|
||||
&mut self._100btc_to_1k_btc,
|
||||
&mut self._1k_btc_to_10k_btc,
|
||||
&mut self._10k_btc_to_100k_btc,
|
||||
&mut self.over_100k_btc,
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Add for AmountRange<T>
|
||||
where
|
||||
T: Add<Output = T>,
|
||||
{
|
||||
type Output = Self;
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self {
|
||||
_0sats: self._0sats + rhs._0sats,
|
||||
_1sat_to_10sats: self._1sat_to_10sats + rhs._1sat_to_10sats,
|
||||
_10sats_to_100sats: self._10sats_to_100sats + rhs._10sats_to_100sats,
|
||||
_100sats_to_1k_sats: self._100sats_to_1k_sats + rhs._100sats_to_1k_sats,
|
||||
_1k_sats_to_10k_sats: self._1k_sats_to_10k_sats + rhs._1k_sats_to_10k_sats,
|
||||
_10k_sats_to_100k_sats: self._10k_sats_to_100k_sats + rhs._10k_sats_to_100k_sats,
|
||||
_100k_sats_to_1m_sats: self._100k_sats_to_1m_sats + rhs._100k_sats_to_1m_sats,
|
||||
_1m_sats_to_10m_sats: self._1m_sats_to_10m_sats + rhs._1m_sats_to_10m_sats,
|
||||
_10m_sats_to_1btc: self._10m_sats_to_1btc + rhs._10m_sats_to_1btc,
|
||||
_1btc_to_10btc: self._1btc_to_10btc + rhs._1btc_to_10btc,
|
||||
_10btc_to_100btc: self._10btc_to_100btc + rhs._10btc_to_100btc,
|
||||
_100btc_to_1k_btc: self._100btc_to_1k_btc + rhs._100btc_to_1k_btc,
|
||||
_1k_btc_to_10k_btc: self._1k_btc_to_10k_btc + rhs._1k_btc_to_10k_btc,
|
||||
_10k_btc_to_100k_btc: self._10k_btc_to_100k_btc + rhs._10k_btc_to_100k_btc,
|
||||
over_100k_btc: self.over_100k_btc + rhs.over_100k_btc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AddAssign for AmountRange<T>
|
||||
where
|
||||
T: AddAssign,
|
||||
{
|
||||
fn add_assign(&mut self, rhs: Self) {
|
||||
self._0sats += rhs._0sats;
|
||||
self._1sat_to_10sats += rhs._1sat_to_10sats;
|
||||
self._10sats_to_100sats += rhs._10sats_to_100sats;
|
||||
self._100sats_to_1k_sats += rhs._100sats_to_1k_sats;
|
||||
self._1k_sats_to_10k_sats += rhs._1k_sats_to_10k_sats;
|
||||
self._10k_sats_to_100k_sats += rhs._10k_sats_to_100k_sats;
|
||||
self._100k_sats_to_1m_sats += rhs._100k_sats_to_1m_sats;
|
||||
self._1m_sats_to_10m_sats += rhs._1m_sats_to_10m_sats;
|
||||
self._10m_sats_to_1btc += rhs._10m_sats_to_1btc;
|
||||
self._1btc_to_10btc += rhs._1btc_to_10btc;
|
||||
self._10btc_to_100btc += rhs._10btc_to_100btc;
|
||||
self._100btc_to_1k_btc += rhs._100btc_to_1k_btc;
|
||||
self._1k_btc_to_10k_btc += rhs._1k_btc_to_10k_btc;
|
||||
self._10k_btc_to_100k_btc += rhs._10k_btc_to_100k_btc;
|
||||
self.over_100k_btc += rhs.over_100k_btc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
use std::ops::{Add, AddAssign};
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::OutputType;
|
||||
use rayon::prelude::*;
|
||||
|
||||
use super::Filter;
|
||||
|
||||
pub const P2PK65: &str = "p2pk65";
|
||||
pub const P2PK33: &str = "p2pk33";
|
||||
pub const P2PKH: &str = "p2pkh";
|
||||
pub const P2SH: &str = "p2sh";
|
||||
pub const P2WPKH: &str = "p2wpkh";
|
||||
pub const P2WSH: &str = "p2wsh";
|
||||
pub const P2TR: &str = "p2tr";
|
||||
pub const P2A: &str = "p2a";
|
||||
|
||||
#[derive(Default, Clone, Debug, Traversable)]
|
||||
pub struct ByAddrType<T> {
|
||||
pub p2pk65: T,
|
||||
pub p2pk33: T,
|
||||
pub p2pkh: T,
|
||||
pub p2sh: T,
|
||||
pub p2wpkh: T,
|
||||
pub p2wsh: T,
|
||||
pub p2tr: T,
|
||||
pub p2a: T,
|
||||
}
|
||||
|
||||
impl<T> ByAddrType<T> {
|
||||
pub fn new<F>(mut create: F) -> Self
|
||||
where
|
||||
F: FnMut(Filter) -> T,
|
||||
{
|
||||
Self {
|
||||
p2pk65: create(Filter::Type(OutputType::P2PK65)),
|
||||
p2pk33: create(Filter::Type(OutputType::P2PK33)),
|
||||
p2pkh: create(Filter::Type(OutputType::P2PKH)),
|
||||
p2sh: create(Filter::Type(OutputType::P2SH)),
|
||||
p2wpkh: create(Filter::Type(OutputType::P2WPKH)),
|
||||
p2wsh: create(Filter::Type(OutputType::P2WSH)),
|
||||
p2tr: create(Filter::Type(OutputType::P2TR)),
|
||||
p2a: create(Filter::Type(OutputType::P2A)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_name<F>(f: F) -> Result<Self>
|
||||
where
|
||||
F: Fn(&'static str) -> Result<T>,
|
||||
{
|
||||
Ok(Self {
|
||||
p2pk65: f(P2PK65)?,
|
||||
p2pk33: f(P2PK33)?,
|
||||
p2pkh: f(P2PKH)?,
|
||||
p2sh: f(P2SH)?,
|
||||
p2wpkh: f(P2WPKH)?,
|
||||
p2wsh: f(P2WSH)?,
|
||||
p2tr: f(P2TR)?,
|
||||
p2a: f(P2A)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn map_with_name<U>(&self, f: impl Fn(&'static str, &T) -> U) -> ByAddrType<U> {
|
||||
ByAddrType {
|
||||
p2pk65: f(P2PK65, &self.p2pk65),
|
||||
p2pk33: f(P2PK33, &self.p2pk33),
|
||||
p2pkh: f(P2PKH, &self.p2pkh),
|
||||
p2sh: f(P2SH, &self.p2sh),
|
||||
p2wpkh: f(P2WPKH, &self.p2wpkh),
|
||||
p2wsh: f(P2WSH, &self.p2wsh),
|
||||
p2tr: f(P2TR, &self.p2tr),
|
||||
p2a: f(P2A, &self.p2a),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_index<F>(f: F) -> Result<Self>
|
||||
where
|
||||
F: Fn(usize) -> Result<T>,
|
||||
{
|
||||
Ok(Self {
|
||||
p2pk65: f(0)?,
|
||||
p2pk33: f(1)?,
|
||||
p2pkh: f(2)?,
|
||||
p2sh: f(3)?,
|
||||
p2wpkh: f(4)?,
|
||||
p2wsh: f(5)?,
|
||||
p2tr: f(6)?,
|
||||
p2a: f(7)?,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_unwrap(&self, addr_type: OutputType) -> &T {
|
||||
self.get(addr_type).unwrap()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get(&self, addr_type: OutputType) -> Option<&T> {
|
||||
match addr_type {
|
||||
OutputType::P2PK65 => Some(&self.p2pk65),
|
||||
OutputType::P2PK33 => Some(&self.p2pk33),
|
||||
OutputType::P2PKH => Some(&self.p2pkh),
|
||||
OutputType::P2SH => Some(&self.p2sh),
|
||||
OutputType::P2WPKH => Some(&self.p2wpkh),
|
||||
OutputType::P2WSH => Some(&self.p2wsh),
|
||||
OutputType::P2TR => Some(&self.p2tr),
|
||||
OutputType::P2A => Some(&self.p2a),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_mut_unwrap(&mut self, addr_type: OutputType) -> &mut T {
|
||||
self.get_mut(addr_type).unwrap()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_mut(&mut self, addr_type: OutputType) -> Option<&mut T> {
|
||||
match addr_type {
|
||||
OutputType::P2PK65 => Some(&mut self.p2pk65),
|
||||
OutputType::P2PK33 => Some(&mut self.p2pk33),
|
||||
OutputType::P2PKH => Some(&mut self.p2pkh),
|
||||
OutputType::P2SH => Some(&mut self.p2sh),
|
||||
OutputType::P2WPKH => Some(&mut self.p2wpkh),
|
||||
OutputType::P2WSH => Some(&mut self.p2wsh),
|
||||
OutputType::P2TR => Some(&mut self.p2tr),
|
||||
OutputType::P2A => Some(&mut self.p2a),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn values(&self) -> impl Iterator<Item = &T> {
|
||||
[
|
||||
&self.p2pk65,
|
||||
&self.p2pk33,
|
||||
&self.p2pkh,
|
||||
&self.p2sh,
|
||||
&self.p2wpkh,
|
||||
&self.p2wsh,
|
||||
&self.p2tr,
|
||||
&self.p2a,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
[
|
||||
&mut self.p2pk65,
|
||||
&mut self.p2pk33,
|
||||
&mut self.p2pkh,
|
||||
&mut self.p2sh,
|
||||
&mut self.p2wpkh,
|
||||
&mut self.p2wsh,
|
||||
&mut self.p2tr,
|
||||
&mut self.p2a,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn par_values(&mut self) -> impl ParallelIterator<Item = &T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
[
|
||||
&self.p2pk65,
|
||||
&self.p2pk33,
|
||||
&self.p2pkh,
|
||||
&self.p2sh,
|
||||
&self.p2wpkh,
|
||||
&self.p2wsh,
|
||||
&self.p2tr,
|
||||
&self.p2a,
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn par_values_mut(&mut self) -> impl ParallelIterator<Item = &mut T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
[
|
||||
&mut self.p2pk65,
|
||||
&mut self.p2pk33,
|
||||
&mut self.p2pkh,
|
||||
&mut self.p2sh,
|
||||
&mut self.p2wpkh,
|
||||
&mut self.p2wsh,
|
||||
&mut self.p2tr,
|
||||
&mut self.p2a,
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn iter(&self) -> impl Iterator<Item = (OutputType, &T)> {
|
||||
[
|
||||
(OutputType::P2PK65, &self.p2pk65),
|
||||
(OutputType::P2PK33, &self.p2pk33),
|
||||
(OutputType::P2PKH, &self.p2pkh),
|
||||
(OutputType::P2SH, &self.p2sh),
|
||||
(OutputType::P2WPKH, &self.p2wpkh),
|
||||
(OutputType::P2WSH, &self.p2wsh),
|
||||
(OutputType::P2TR, &self.p2tr),
|
||||
(OutputType::P2A, &self.p2a),
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn into_iter(self) -> impl Iterator<Item = (OutputType, T)> {
|
||||
[
|
||||
(OutputType::P2PK65, self.p2pk65),
|
||||
(OutputType::P2PK33, self.p2pk33),
|
||||
(OutputType::P2PKH, self.p2pkh),
|
||||
(OutputType::P2SH, self.p2sh),
|
||||
(OutputType::P2WPKH, self.p2wpkh),
|
||||
(OutputType::P2WSH, self.p2wsh),
|
||||
(OutputType::P2TR, self.p2tr),
|
||||
(OutputType::P2A, self.p2a),
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = (OutputType, &mut T)> {
|
||||
[
|
||||
(OutputType::P2PK65, &mut self.p2pk65),
|
||||
(OutputType::P2PK33, &mut self.p2pk33),
|
||||
(OutputType::P2PKH, &mut self.p2pkh),
|
||||
(OutputType::P2SH, &mut self.p2sh),
|
||||
(OutputType::P2WPKH, &mut self.p2wpkh),
|
||||
(OutputType::P2WSH, &mut self.p2wsh),
|
||||
(OutputType::P2TR, &mut self.p2tr),
|
||||
(OutputType::P2A, &mut self.p2a),
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Add for ByAddrType<T>
|
||||
where
|
||||
T: Add<Output = T>,
|
||||
{
|
||||
type Output = Self;
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self {
|
||||
p2pk65: self.p2pk65 + rhs.p2pk65,
|
||||
p2pk33: self.p2pk33 + rhs.p2pk33,
|
||||
p2pkh: self.p2pkh + rhs.p2pkh,
|
||||
p2sh: self.p2sh + rhs.p2sh,
|
||||
p2wpkh: self.p2wpkh + rhs.p2wpkh,
|
||||
p2wsh: self.p2wsh + rhs.p2wsh,
|
||||
p2tr: self.p2tr + rhs.p2tr,
|
||||
p2a: self.p2a + rhs.p2a,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AddAssign for ByAddrType<T>
|
||||
where
|
||||
T: AddAssign,
|
||||
{
|
||||
fn add_assign(&mut self, rhs: Self) {
|
||||
self.p2pk65 += rhs.p2pk65;
|
||||
self.p2pk33 += rhs.p2pk33;
|
||||
self.p2pkh += rhs.p2pkh;
|
||||
self.p2sh += rhs.p2sh;
|
||||
self.p2wpkh += rhs.p2wpkh;
|
||||
self.p2wsh += rhs.p2wsh;
|
||||
self.p2tr += rhs.p2tr;
|
||||
self.p2a += rhs.p2a;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ByAddrType<Option<T>> {
|
||||
pub fn take(&mut self) {
|
||||
self.values_mut().for_each(|opt| {
|
||||
opt.take();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Zip one ByAddrType with a function, producing a new ByAddrType.
|
||||
pub fn zip_by_addr_type<S, R, F>(source: &ByAddrType<S>, f: F) -> Result<ByAddrType<R>>
|
||||
where
|
||||
F: Fn(&'static str, &S) -> Result<R>,
|
||||
{
|
||||
Ok(ByAddrType {
|
||||
p2pk65: f(P2PK65, &source.p2pk65)?,
|
||||
p2pk33: f(P2PK33, &source.p2pk33)?,
|
||||
p2pkh: f(P2PKH, &source.p2pkh)?,
|
||||
p2sh: f(P2SH, &source.p2sh)?,
|
||||
p2wpkh: f(P2WPKH, &source.p2wpkh)?,
|
||||
p2wsh: f(P2WSH, &source.p2wsh)?,
|
||||
p2tr: f(P2TR, &source.p2tr)?,
|
||||
p2a: f(P2A, &source.p2a)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Zip two ByAddrTypes with a function, producing a new ByAddrType.
|
||||
pub fn zip2_by_addr_type<S1, S2, R, F>(
|
||||
a: &ByAddrType<S1>,
|
||||
b: &ByAddrType<S2>,
|
||||
f: F,
|
||||
) -> Result<ByAddrType<R>>
|
||||
where
|
||||
F: Fn(&'static str, &S1, &S2) -> Result<R>,
|
||||
{
|
||||
Ok(ByAddrType {
|
||||
p2pk65: f(P2PK65, &a.p2pk65, &b.p2pk65)?,
|
||||
p2pk33: f(P2PK33, &a.p2pk33, &b.p2pk33)?,
|
||||
p2pkh: f(P2PKH, &a.p2pkh, &b.p2pkh)?,
|
||||
p2sh: f(P2SH, &a.p2sh, &b.p2sh)?,
|
||||
p2wpkh: f(P2WPKH, &a.p2wpkh, &b.p2wpkh)?,
|
||||
p2wsh: f(P2WSH, &a.p2wsh, &b.p2wsh)?,
|
||||
p2tr: f(P2TR, &a.p2tr, &b.p2tr)?,
|
||||
p2a: f(P2A, &a.p2a, &b.p2a)?,
|
||||
})
|
||||
}
|
||||
@@ -1,290 +0,0 @@
|
||||
use std::ops::{Add, AddAssign};
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::OutputType;
|
||||
use rayon::prelude::*;
|
||||
|
||||
use super::Filter;
|
||||
|
||||
pub const P2PK65: &str = "p2pk65";
|
||||
pub const P2PK33: &str = "p2pk33";
|
||||
pub const P2PKH: &str = "p2pkh";
|
||||
pub const P2SH: &str = "p2sh";
|
||||
pub const P2WPKH: &str = "p2wpkh";
|
||||
pub const P2WSH: &str = "p2wsh";
|
||||
pub const P2TR: &str = "p2tr";
|
||||
pub const P2A: &str = "p2a";
|
||||
|
||||
#[derive(Default, Clone, Debug, Traversable)]
|
||||
pub struct ByAddressType<T> {
|
||||
pub p2pk65: T,
|
||||
pub p2pk33: T,
|
||||
pub p2pkh: T,
|
||||
pub p2sh: T,
|
||||
pub p2wpkh: T,
|
||||
pub p2wsh: T,
|
||||
pub p2tr: T,
|
||||
pub p2a: T,
|
||||
}
|
||||
|
||||
impl<T> ByAddressType<T> {
|
||||
pub fn new<F>(mut create: F) -> Self
|
||||
where
|
||||
F: FnMut(Filter) -> T,
|
||||
{
|
||||
Self {
|
||||
p2pk65: create(Filter::Type(OutputType::P2PK65)),
|
||||
p2pk33: create(Filter::Type(OutputType::P2PK33)),
|
||||
p2pkh: create(Filter::Type(OutputType::P2PKH)),
|
||||
p2sh: create(Filter::Type(OutputType::P2SH)),
|
||||
p2wpkh: create(Filter::Type(OutputType::P2WPKH)),
|
||||
p2wsh: create(Filter::Type(OutputType::P2WSH)),
|
||||
p2tr: create(Filter::Type(OutputType::P2TR)),
|
||||
p2a: create(Filter::Type(OutputType::P2A)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_name<F>(f: F) -> Result<Self>
|
||||
where
|
||||
F: Fn(&'static str) -> Result<T>,
|
||||
{
|
||||
Ok(Self {
|
||||
p2pk65: f(P2PK65)?,
|
||||
p2pk33: f(P2PK33)?,
|
||||
p2pkh: f(P2PKH)?,
|
||||
p2sh: f(P2SH)?,
|
||||
p2wpkh: f(P2WPKH)?,
|
||||
p2wsh: f(P2WSH)?,
|
||||
p2tr: f(P2TR)?,
|
||||
p2a: f(P2A)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_with_index<F>(f: F) -> Result<Self>
|
||||
where
|
||||
F: Fn(usize) -> Result<T>,
|
||||
{
|
||||
Ok(Self {
|
||||
p2pk65: f(0)?,
|
||||
p2pk33: f(1)?,
|
||||
p2pkh: f(2)?,
|
||||
p2sh: f(3)?,
|
||||
p2wpkh: f(4)?,
|
||||
p2wsh: f(5)?,
|
||||
p2tr: f(6)?,
|
||||
p2a: f(7)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn try_zip_with_name<S, R, F>(other: &ByAddressType<S>, f: F) -> Result<ByAddressType<R>>
|
||||
where
|
||||
F: Fn(&'static str, &S) -> Result<R>,
|
||||
{
|
||||
Ok(ByAddressType {
|
||||
p2pk65: f(P2PK65, &other.p2pk65)?,
|
||||
p2pk33: f(P2PK33, &other.p2pk33)?,
|
||||
p2pkh: f(P2PKH, &other.p2pkh)?,
|
||||
p2sh: f(P2SH, &other.p2sh)?,
|
||||
p2wpkh: f(P2WPKH, &other.p2wpkh)?,
|
||||
p2wsh: f(P2WSH, &other.p2wsh)?,
|
||||
p2tr: f(P2TR, &other.p2tr)?,
|
||||
p2a: f(P2A, &other.p2a)?,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_unwrap(&self, addresstype: OutputType) -> &T {
|
||||
self.get(addresstype).unwrap()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get(&self, address_type: OutputType) -> Option<&T> {
|
||||
match address_type {
|
||||
OutputType::P2PK65 => Some(&self.p2pk65),
|
||||
OutputType::P2PK33 => Some(&self.p2pk33),
|
||||
OutputType::P2PKH => Some(&self.p2pkh),
|
||||
OutputType::P2SH => Some(&self.p2sh),
|
||||
OutputType::P2WPKH => Some(&self.p2wpkh),
|
||||
OutputType::P2WSH => Some(&self.p2wsh),
|
||||
OutputType::P2TR => Some(&self.p2tr),
|
||||
OutputType::P2A => Some(&self.p2a),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_mut_unwrap(&mut self, addresstype: OutputType) -> &mut T {
|
||||
self.get_mut(addresstype).unwrap()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_mut(&mut self, address_type: OutputType) -> Option<&mut T> {
|
||||
match address_type {
|
||||
OutputType::P2PK65 => Some(&mut self.p2pk65),
|
||||
OutputType::P2PK33 => Some(&mut self.p2pk33),
|
||||
OutputType::P2PKH => Some(&mut self.p2pkh),
|
||||
OutputType::P2SH => Some(&mut self.p2sh),
|
||||
OutputType::P2WPKH => Some(&mut self.p2wpkh),
|
||||
OutputType::P2WSH => Some(&mut self.p2wsh),
|
||||
OutputType::P2TR => Some(&mut self.p2tr),
|
||||
OutputType::P2A => Some(&mut self.p2a),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn values(&self) -> impl Iterator<Item = &T> {
|
||||
[
|
||||
&self.p2pk65,
|
||||
&self.p2pk33,
|
||||
&self.p2pkh,
|
||||
&self.p2sh,
|
||||
&self.p2wpkh,
|
||||
&self.p2wsh,
|
||||
&self.p2tr,
|
||||
&self.p2a,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
[
|
||||
&mut self.p2pk65,
|
||||
&mut self.p2pk33,
|
||||
&mut self.p2pkh,
|
||||
&mut self.p2sh,
|
||||
&mut self.p2wpkh,
|
||||
&mut self.p2wsh,
|
||||
&mut self.p2tr,
|
||||
&mut self.p2a,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn par_values(&mut self) -> impl ParallelIterator<Item = &T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
[
|
||||
&self.p2pk65,
|
||||
&self.p2pk33,
|
||||
&self.p2pkh,
|
||||
&self.p2sh,
|
||||
&self.p2wpkh,
|
||||
&self.p2wsh,
|
||||
&self.p2tr,
|
||||
&self.p2a,
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn par_values_mut(&mut self) -> impl ParallelIterator<Item = &mut T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
[
|
||||
&mut self.p2pk65,
|
||||
&mut self.p2pk33,
|
||||
&mut self.p2pkh,
|
||||
&mut self.p2sh,
|
||||
&mut self.p2wpkh,
|
||||
&mut self.p2wsh,
|
||||
&mut self.p2tr,
|
||||
&mut self.p2a,
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn iter(&self) -> impl Iterator<Item = (OutputType, &T)> {
|
||||
[
|
||||
(OutputType::P2PK65, &self.p2pk65),
|
||||
(OutputType::P2PK33, &self.p2pk33),
|
||||
(OutputType::P2PKH, &self.p2pkh),
|
||||
(OutputType::P2SH, &self.p2sh),
|
||||
(OutputType::P2WPKH, &self.p2wpkh),
|
||||
(OutputType::P2WSH, &self.p2wsh),
|
||||
(OutputType::P2TR, &self.p2tr),
|
||||
(OutputType::P2A, &self.p2a),
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn into_iter(self) -> impl Iterator<Item = (OutputType, T)> {
|
||||
[
|
||||
(OutputType::P2PK65, self.p2pk65),
|
||||
(OutputType::P2PK33, self.p2pk33),
|
||||
(OutputType::P2PKH, self.p2pkh),
|
||||
(OutputType::P2SH, self.p2sh),
|
||||
(OutputType::P2WPKH, self.p2wpkh),
|
||||
(OutputType::P2WSH, self.p2wsh),
|
||||
(OutputType::P2TR, self.p2tr),
|
||||
(OutputType::P2A, self.p2a),
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = (OutputType, &mut T)> {
|
||||
[
|
||||
(OutputType::P2PK65, &mut self.p2pk65),
|
||||
(OutputType::P2PK33, &mut self.p2pk33),
|
||||
(OutputType::P2PKH, &mut self.p2pkh),
|
||||
(OutputType::P2SH, &mut self.p2sh),
|
||||
(OutputType::P2WPKH, &mut self.p2wpkh),
|
||||
(OutputType::P2WSH, &mut self.p2wsh),
|
||||
(OutputType::P2TR, &mut self.p2tr),
|
||||
(OutputType::P2A, &mut self.p2a),
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Add for ByAddressType<T>
|
||||
where
|
||||
T: Add<Output = T>,
|
||||
{
|
||||
type Output = Self;
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self {
|
||||
p2pk65: self.p2pk65 + rhs.p2pk65,
|
||||
p2pk33: self.p2pk33 + rhs.p2pk33,
|
||||
p2pkh: self.p2pkh + rhs.p2pkh,
|
||||
p2sh: self.p2sh + rhs.p2sh,
|
||||
p2wpkh: self.p2wpkh + rhs.p2wpkh,
|
||||
p2wsh: self.p2wsh + rhs.p2wsh,
|
||||
p2tr: self.p2tr + rhs.p2tr,
|
||||
p2a: self.p2a + rhs.p2a,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AddAssign for ByAddressType<T>
|
||||
where
|
||||
T: AddAssign,
|
||||
{
|
||||
fn add_assign(&mut self, rhs: Self) {
|
||||
self.p2pk65 += rhs.p2pk65;
|
||||
self.p2pk33 += rhs.p2pk33;
|
||||
self.p2pkh += rhs.p2pkh;
|
||||
self.p2sh += rhs.p2sh;
|
||||
self.p2wpkh += rhs.p2wpkh;
|
||||
self.p2wsh += rhs.p2wsh;
|
||||
self.p2tr += rhs.p2tr;
|
||||
self.p2a += rhs.p2a;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ByAddressType<Option<T>> {
|
||||
pub fn take(&mut self) {
|
||||
self.values_mut().for_each(|opt| {
|
||||
opt.take();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,348 +0,0 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use brk_types::Age;
|
||||
use brk_traversable::Traversable;
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use serde::Serialize;
|
||||
|
||||
use super::{CohortName, Filter, TimeFilter};
|
||||
|
||||
// Age boundary constants in hours
|
||||
pub const HOURS_1H: usize = 1;
|
||||
pub const HOURS_1D: usize = 24;
|
||||
pub const HOURS_1W: usize = 24 * 7;
|
||||
pub const HOURS_1M: usize = 24 * 30;
|
||||
pub const HOURS_2M: usize = 24 * 2 * 30;
|
||||
pub const HOURS_3M: usize = 24 * 3 * 30;
|
||||
pub const HOURS_4M: usize = 24 * 4 * 30;
|
||||
pub const HOURS_5M: usize = 24 * 5 * 30; // STH/LTH threshold
|
||||
pub const HOURS_6M: usize = 24 * 6 * 30;
|
||||
pub const HOURS_1Y: usize = 24 * 365;
|
||||
pub const HOURS_2Y: usize = 24 * 2 * 365;
|
||||
pub const HOURS_3Y: usize = 24 * 3 * 365;
|
||||
pub const HOURS_4Y: usize = 24 * 4 * 365;
|
||||
pub const HOURS_5Y: usize = 24 * 5 * 365;
|
||||
pub const HOURS_6Y: usize = 24 * 6 * 365;
|
||||
pub const HOURS_7Y: usize = 24 * 7 * 365;
|
||||
pub const HOURS_8Y: usize = 24 * 8 * 365;
|
||||
pub const HOURS_10Y: usize = 24 * 10 * 365;
|
||||
pub const HOURS_12Y: usize = 24 * 12 * 365;
|
||||
pub const HOURS_15Y: usize = 24 * 15 * 365;
|
||||
|
||||
/// Age boundaries in hours. Defines the cohort ranges:
|
||||
/// [0, 1h), [1h, 1d), [1d, 1w), [1w, 1m), ..., [15y, ∞)
|
||||
pub const AGE_BOUNDARIES: [usize; 20] = [
|
||||
HOURS_1H, HOURS_1D, HOURS_1W, HOURS_1M, HOURS_2M, HOURS_3M, HOURS_4M,
|
||||
HOURS_5M, HOURS_6M, HOURS_1Y, HOURS_2Y, HOURS_3Y, HOURS_4Y, HOURS_5Y,
|
||||
HOURS_6Y, HOURS_7Y, HOURS_8Y, HOURS_10Y, HOURS_12Y, HOURS_15Y,
|
||||
];
|
||||
|
||||
/// Age range bounds (end = usize::MAX means unbounded)
|
||||
pub const AGE_RANGE_BOUNDS: ByAgeRange<Range<usize>> = ByAgeRange {
|
||||
up_to_1h: 0..HOURS_1H,
|
||||
_1h_to_1d: HOURS_1H..HOURS_1D,
|
||||
_1d_to_1w: HOURS_1D..HOURS_1W,
|
||||
_1w_to_1m: HOURS_1W..HOURS_1M,
|
||||
_1m_to_2m: HOURS_1M..HOURS_2M,
|
||||
_2m_to_3m: HOURS_2M..HOURS_3M,
|
||||
_3m_to_4m: HOURS_3M..HOURS_4M,
|
||||
_4m_to_5m: HOURS_4M..HOURS_5M,
|
||||
_5m_to_6m: HOURS_5M..HOURS_6M,
|
||||
_6m_to_1y: HOURS_6M..HOURS_1Y,
|
||||
_1y_to_2y: HOURS_1Y..HOURS_2Y,
|
||||
_2y_to_3y: HOURS_2Y..HOURS_3Y,
|
||||
_3y_to_4y: HOURS_3Y..HOURS_4Y,
|
||||
_4y_to_5y: HOURS_4Y..HOURS_5Y,
|
||||
_5y_to_6y: HOURS_5Y..HOURS_6Y,
|
||||
_6y_to_7y: HOURS_6Y..HOURS_7Y,
|
||||
_7y_to_8y: HOURS_7Y..HOURS_8Y,
|
||||
_8y_to_10y: HOURS_8Y..HOURS_10Y,
|
||||
_10y_to_12y: HOURS_10Y..HOURS_12Y,
|
||||
_12y_to_15y: HOURS_12Y..HOURS_15Y,
|
||||
from_15y: HOURS_15Y..usize::MAX,
|
||||
};
|
||||
|
||||
/// Age range filters
|
||||
pub const AGE_RANGE_FILTERS: ByAgeRange<Filter> = ByAgeRange {
|
||||
up_to_1h: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS.up_to_1h)),
|
||||
_1h_to_1d: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._1h_to_1d)),
|
||||
_1d_to_1w: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._1d_to_1w)),
|
||||
_1w_to_1m: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._1w_to_1m)),
|
||||
_1m_to_2m: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._1m_to_2m)),
|
||||
_2m_to_3m: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._2m_to_3m)),
|
||||
_3m_to_4m: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._3m_to_4m)),
|
||||
_4m_to_5m: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._4m_to_5m)),
|
||||
_5m_to_6m: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._5m_to_6m)),
|
||||
_6m_to_1y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._6m_to_1y)),
|
||||
_1y_to_2y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._1y_to_2y)),
|
||||
_2y_to_3y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._2y_to_3y)),
|
||||
_3y_to_4y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._3y_to_4y)),
|
||||
_4y_to_5y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._4y_to_5y)),
|
||||
_5y_to_6y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._5y_to_6y)),
|
||||
_6y_to_7y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._6y_to_7y)),
|
||||
_7y_to_8y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._7y_to_8y)),
|
||||
_8y_to_10y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._8y_to_10y)),
|
||||
_10y_to_12y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._10y_to_12y)),
|
||||
_12y_to_15y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS._12y_to_15y)),
|
||||
from_15y: Filter::Time(TimeFilter::Range(AGE_RANGE_BOUNDS.from_15y)),
|
||||
};
|
||||
|
||||
/// Age range names
|
||||
pub const AGE_RANGE_NAMES: ByAgeRange<CohortName> = ByAgeRange {
|
||||
up_to_1h: CohortName::new("up_to_1h_old", "<1h", "Up to 1 Hour Old"),
|
||||
_1h_to_1d: CohortName::new("at_least_1h_up_to_1d_old", "1h-1d", "1 Hour to 1 Day Old"),
|
||||
_1d_to_1w: CohortName::new("at_least_1d_up_to_1w_old", "1d-1w", "1 Day to 1 Week Old"),
|
||||
_1w_to_1m: CohortName::new("at_least_1w_up_to_1m_old", "1w-1m", "1 Week to 1 Month Old"),
|
||||
_1m_to_2m: CohortName::new("at_least_1m_up_to_2m_old", "1m-2m", "1 to 2 Months Old"),
|
||||
_2m_to_3m: CohortName::new("at_least_2m_up_to_3m_old", "2m-3m", "2 to 3 Months Old"),
|
||||
_3m_to_4m: CohortName::new("at_least_3m_up_to_4m_old", "3m-4m", "3 to 4 Months Old"),
|
||||
_4m_to_5m: CohortName::new("at_least_4m_up_to_5m_old", "4m-5m", "4 to 5 Months Old"),
|
||||
_5m_to_6m: CohortName::new("at_least_5m_up_to_6m_old", "5m-6m", "5 to 6 Months Old"),
|
||||
_6m_to_1y: CohortName::new("at_least_6m_up_to_1y_old", "6m-1y", "6 Months to 1 Year Old"),
|
||||
_1y_to_2y: CohortName::new("at_least_1y_up_to_2y_old", "1y-2y", "1 to 2 Years Old"),
|
||||
_2y_to_3y: CohortName::new("at_least_2y_up_to_3y_old", "2y-3y", "2 to 3 Years Old"),
|
||||
_3y_to_4y: CohortName::new("at_least_3y_up_to_4y_old", "3y-4y", "3 to 4 Years Old"),
|
||||
_4y_to_5y: CohortName::new("at_least_4y_up_to_5y_old", "4y-5y", "4 to 5 Years Old"),
|
||||
_5y_to_6y: CohortName::new("at_least_5y_up_to_6y_old", "5y-6y", "5 to 6 Years Old"),
|
||||
_6y_to_7y: CohortName::new("at_least_6y_up_to_7y_old", "6y-7y", "6 to 7 Years Old"),
|
||||
_7y_to_8y: CohortName::new("at_least_7y_up_to_8y_old", "7y-8y", "7 to 8 Years Old"),
|
||||
_8y_to_10y: CohortName::new("at_least_8y_up_to_10y_old", "8y-10y", "8 to 10 Years Old"),
|
||||
_10y_to_12y: CohortName::new("at_least_10y_up_to_12y_old", "10y-12y", "10 to 12 Years Old"),
|
||||
_12y_to_15y: CohortName::new("at_least_12y_up_to_15y_old", "12y-15y", "12 to 15 Years Old"),
|
||||
from_15y: CohortName::new("at_least_15y_old", "15y+", "15+ Years Old"),
|
||||
};
|
||||
|
||||
impl ByAgeRange<CohortName> {
|
||||
pub const fn names() -> &'static Self {
|
||||
&AGE_RANGE_NAMES
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Traversable, Serialize)]
|
||||
pub struct ByAgeRange<T> {
|
||||
pub up_to_1h: T,
|
||||
pub _1h_to_1d: T,
|
||||
pub _1d_to_1w: T,
|
||||
pub _1w_to_1m: T,
|
||||
pub _1m_to_2m: T,
|
||||
pub _2m_to_3m: T,
|
||||
pub _3m_to_4m: T,
|
||||
pub _4m_to_5m: T,
|
||||
pub _5m_to_6m: T,
|
||||
pub _6m_to_1y: T,
|
||||
pub _1y_to_2y: T,
|
||||
pub _2y_to_3y: T,
|
||||
pub _3y_to_4y: T,
|
||||
pub _4y_to_5y: T,
|
||||
pub _5y_to_6y: T,
|
||||
pub _6y_to_7y: T,
|
||||
pub _7y_to_8y: T,
|
||||
pub _8y_to_10y: T,
|
||||
pub _10y_to_12y: T,
|
||||
pub _12y_to_15y: T,
|
||||
pub from_15y: T,
|
||||
}
|
||||
|
||||
impl<T> ByAgeRange<T> {
|
||||
/// Get mutable reference by Age. O(1).
|
||||
#[inline]
|
||||
pub fn get_mut(&mut self, age: Age) -> &mut T {
|
||||
match age.hours() {
|
||||
0..HOURS_1H => &mut self.up_to_1h,
|
||||
HOURS_1H..HOURS_1D => &mut self._1h_to_1d,
|
||||
HOURS_1D..HOURS_1W => &mut self._1d_to_1w,
|
||||
HOURS_1W..HOURS_1M => &mut self._1w_to_1m,
|
||||
HOURS_1M..HOURS_2M => &mut self._1m_to_2m,
|
||||
HOURS_2M..HOURS_3M => &mut self._2m_to_3m,
|
||||
HOURS_3M..HOURS_4M => &mut self._3m_to_4m,
|
||||
HOURS_4M..HOURS_5M => &mut self._4m_to_5m,
|
||||
HOURS_5M..HOURS_6M => &mut self._5m_to_6m,
|
||||
HOURS_6M..HOURS_1Y => &mut self._6m_to_1y,
|
||||
HOURS_1Y..HOURS_2Y => &mut self._1y_to_2y,
|
||||
HOURS_2Y..HOURS_3Y => &mut self._2y_to_3y,
|
||||
HOURS_3Y..HOURS_4Y => &mut self._3y_to_4y,
|
||||
HOURS_4Y..HOURS_5Y => &mut self._4y_to_5y,
|
||||
HOURS_5Y..HOURS_6Y => &mut self._5y_to_6y,
|
||||
HOURS_6Y..HOURS_7Y => &mut self._6y_to_7y,
|
||||
HOURS_7Y..HOURS_8Y => &mut self._7y_to_8y,
|
||||
HOURS_8Y..HOURS_10Y => &mut self._8y_to_10y,
|
||||
HOURS_10Y..HOURS_12Y => &mut self._10y_to_12y,
|
||||
HOURS_12Y..HOURS_15Y => &mut self._12y_to_15y,
|
||||
_ => &mut self.from_15y,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get reference by Age. O(1).
|
||||
#[inline]
|
||||
pub fn get(&self, age: Age) -> &T {
|
||||
match age.hours() {
|
||||
0..HOURS_1H => &self.up_to_1h,
|
||||
HOURS_1H..HOURS_1D => &self._1h_to_1d,
|
||||
HOURS_1D..HOURS_1W => &self._1d_to_1w,
|
||||
HOURS_1W..HOURS_1M => &self._1w_to_1m,
|
||||
HOURS_1M..HOURS_2M => &self._1m_to_2m,
|
||||
HOURS_2M..HOURS_3M => &self._2m_to_3m,
|
||||
HOURS_3M..HOURS_4M => &self._3m_to_4m,
|
||||
HOURS_4M..HOURS_5M => &self._4m_to_5m,
|
||||
HOURS_5M..HOURS_6M => &self._5m_to_6m,
|
||||
HOURS_6M..HOURS_1Y => &self._6m_to_1y,
|
||||
HOURS_1Y..HOURS_2Y => &self._1y_to_2y,
|
||||
HOURS_2Y..HOURS_3Y => &self._2y_to_3y,
|
||||
HOURS_3Y..HOURS_4Y => &self._3y_to_4y,
|
||||
HOURS_4Y..HOURS_5Y => &self._4y_to_5y,
|
||||
HOURS_5Y..HOURS_6Y => &self._5y_to_6y,
|
||||
HOURS_6Y..HOURS_7Y => &self._6y_to_7y,
|
||||
HOURS_7Y..HOURS_8Y => &self._7y_to_8y,
|
||||
HOURS_8Y..HOURS_10Y => &self._8y_to_10y,
|
||||
HOURS_10Y..HOURS_12Y => &self._10y_to_12y,
|
||||
HOURS_12Y..HOURS_15Y => &self._12y_to_15y,
|
||||
_ => &self.from_15y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new<F>(mut create: F) -> Self
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> T,
|
||||
{
|
||||
let f = AGE_RANGE_FILTERS;
|
||||
let n = AGE_RANGE_NAMES;
|
||||
Self {
|
||||
up_to_1h: create(f.up_to_1h.clone(), n.up_to_1h.id),
|
||||
_1h_to_1d: create(f._1h_to_1d.clone(), n._1h_to_1d.id),
|
||||
_1d_to_1w: create(f._1d_to_1w.clone(), n._1d_to_1w.id),
|
||||
_1w_to_1m: create(f._1w_to_1m.clone(), n._1w_to_1m.id),
|
||||
_1m_to_2m: create(f._1m_to_2m.clone(), n._1m_to_2m.id),
|
||||
_2m_to_3m: create(f._2m_to_3m.clone(), n._2m_to_3m.id),
|
||||
_3m_to_4m: create(f._3m_to_4m.clone(), n._3m_to_4m.id),
|
||||
_4m_to_5m: create(f._4m_to_5m.clone(), n._4m_to_5m.id),
|
||||
_5m_to_6m: create(f._5m_to_6m.clone(), n._5m_to_6m.id),
|
||||
_6m_to_1y: create(f._6m_to_1y.clone(), n._6m_to_1y.id),
|
||||
_1y_to_2y: create(f._1y_to_2y.clone(), n._1y_to_2y.id),
|
||||
_2y_to_3y: create(f._2y_to_3y.clone(), n._2y_to_3y.id),
|
||||
_3y_to_4y: create(f._3y_to_4y.clone(), n._3y_to_4y.id),
|
||||
_4y_to_5y: create(f._4y_to_5y.clone(), n._4y_to_5y.id),
|
||||
_5y_to_6y: create(f._5y_to_6y.clone(), n._5y_to_6y.id),
|
||||
_6y_to_7y: create(f._6y_to_7y.clone(), n._6y_to_7y.id),
|
||||
_7y_to_8y: create(f._7y_to_8y.clone(), n._7y_to_8y.id),
|
||||
_8y_to_10y: create(f._8y_to_10y.clone(), n._8y_to_10y.id),
|
||||
_10y_to_12y: create(f._10y_to_12y.clone(), n._10y_to_12y.id),
|
||||
_12y_to_15y: create(f._12y_to_15y.clone(), n._12y_to_15y.id),
|
||||
from_15y: create(f.from_15y.clone(), n.from_15y.id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_new<F, E>(mut create: F) -> Result<Self, E>
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> Result<T, E>,
|
||||
{
|
||||
let f = AGE_RANGE_FILTERS;
|
||||
let n = AGE_RANGE_NAMES;
|
||||
Ok(Self {
|
||||
up_to_1h: create(f.up_to_1h.clone(), n.up_to_1h.id)?,
|
||||
_1h_to_1d: create(f._1h_to_1d.clone(), n._1h_to_1d.id)?,
|
||||
_1d_to_1w: create(f._1d_to_1w.clone(), n._1d_to_1w.id)?,
|
||||
_1w_to_1m: create(f._1w_to_1m.clone(), n._1w_to_1m.id)?,
|
||||
_1m_to_2m: create(f._1m_to_2m.clone(), n._1m_to_2m.id)?,
|
||||
_2m_to_3m: create(f._2m_to_3m.clone(), n._2m_to_3m.id)?,
|
||||
_3m_to_4m: create(f._3m_to_4m.clone(), n._3m_to_4m.id)?,
|
||||
_4m_to_5m: create(f._4m_to_5m.clone(), n._4m_to_5m.id)?,
|
||||
_5m_to_6m: create(f._5m_to_6m.clone(), n._5m_to_6m.id)?,
|
||||
_6m_to_1y: create(f._6m_to_1y.clone(), n._6m_to_1y.id)?,
|
||||
_1y_to_2y: create(f._1y_to_2y.clone(), n._1y_to_2y.id)?,
|
||||
_2y_to_3y: create(f._2y_to_3y.clone(), n._2y_to_3y.id)?,
|
||||
_3y_to_4y: create(f._3y_to_4y.clone(), n._3y_to_4y.id)?,
|
||||
_4y_to_5y: create(f._4y_to_5y.clone(), n._4y_to_5y.id)?,
|
||||
_5y_to_6y: create(f._5y_to_6y.clone(), n._5y_to_6y.id)?,
|
||||
_6y_to_7y: create(f._6y_to_7y.clone(), n._6y_to_7y.id)?,
|
||||
_7y_to_8y: create(f._7y_to_8y.clone(), n._7y_to_8y.id)?,
|
||||
_8y_to_10y: create(f._8y_to_10y.clone(), n._8y_to_10y.id)?,
|
||||
_10y_to_12y: create(f._10y_to_12y.clone(), n._10y_to_12y.id)?,
|
||||
_12y_to_15y: create(f._12y_to_15y.clone(), n._12y_to_15y.id)?,
|
||||
from_15y: create(f.from_15y.clone(), n.from_15y.id)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
[
|
||||
&self.up_to_1h,
|
||||
&self._1h_to_1d,
|
||||
&self._1d_to_1w,
|
||||
&self._1w_to_1m,
|
||||
&self._1m_to_2m,
|
||||
&self._2m_to_3m,
|
||||
&self._3m_to_4m,
|
||||
&self._4m_to_5m,
|
||||
&self._5m_to_6m,
|
||||
&self._6m_to_1y,
|
||||
&self._1y_to_2y,
|
||||
&self._2y_to_3y,
|
||||
&self._3y_to_4y,
|
||||
&self._4y_to_5y,
|
||||
&self._5y_to_6y,
|
||||
&self._6y_to_7y,
|
||||
&self._7y_to_8y,
|
||||
&self._8y_to_10y,
|
||||
&self._10y_to_12y,
|
||||
&self._12y_to_15y,
|
||||
&self.from_15y,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
[
|
||||
&mut self.up_to_1h,
|
||||
&mut self._1h_to_1d,
|
||||
&mut self._1d_to_1w,
|
||||
&mut self._1w_to_1m,
|
||||
&mut self._1m_to_2m,
|
||||
&mut self._2m_to_3m,
|
||||
&mut self._3m_to_4m,
|
||||
&mut self._4m_to_5m,
|
||||
&mut self._5m_to_6m,
|
||||
&mut self._6m_to_1y,
|
||||
&mut self._1y_to_2y,
|
||||
&mut self._2y_to_3y,
|
||||
&mut self._3y_to_4y,
|
||||
&mut self._4y_to_5y,
|
||||
&mut self._5y_to_6y,
|
||||
&mut self._6y_to_7y,
|
||||
&mut self._7y_to_8y,
|
||||
&mut self._8y_to_10y,
|
||||
&mut self._10y_to_12y,
|
||||
&mut self._12y_to_15y,
|
||||
&mut self.from_15y,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
[
|
||||
&mut self.up_to_1h,
|
||||
&mut self._1h_to_1d,
|
||||
&mut self._1d_to_1w,
|
||||
&mut self._1w_to_1m,
|
||||
&mut self._1m_to_2m,
|
||||
&mut self._2m_to_3m,
|
||||
&mut self._3m_to_4m,
|
||||
&mut self._4m_to_5m,
|
||||
&mut self._5m_to_6m,
|
||||
&mut self._6m_to_1y,
|
||||
&mut self._1y_to_2y,
|
||||
&mut self._2y_to_3y,
|
||||
&mut self._3y_to_4y,
|
||||
&mut self._4y_to_5y,
|
||||
&mut self._5y_to_6y,
|
||||
&mut self._6y_to_7y,
|
||||
&mut self._7y_to_8y,
|
||||
&mut self._8y_to_10y,
|
||||
&mut self._10y_to_12y,
|
||||
&mut self._12y_to_15y,
|
||||
&mut self.from_15y,
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
}
|
||||
@@ -1,443 +0,0 @@
|
||||
use std::ops::{Add, AddAssign, Range};
|
||||
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::Sats;
|
||||
use rayon::prelude::*;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::{AmountFilter, CohortName, Filter};
|
||||
|
||||
/// Bucket index for amount ranges. Use for cheap comparisons and direct lookups.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct AmountBucket(u8);
|
||||
|
||||
impl AmountBucket {
|
||||
/// Returns (self, other) if buckets differ, None if same.
|
||||
/// Use with `ByAmountRange::get_mut_by_bucket` to avoid recomputing.
|
||||
#[inline(always)]
|
||||
pub fn transition_to(self, other: Self) -> Option<(Self, Self)> {
|
||||
if self != other {
|
||||
Some((self, other))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn index(self) -> u8 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Sats> for AmountBucket {
|
||||
#[inline(always)]
|
||||
fn from(value: Sats) -> Self {
|
||||
Self(match value {
|
||||
v if v < Sats::_1 => 0,
|
||||
v if v < Sats::_10 => 1,
|
||||
v if v < Sats::_100 => 2,
|
||||
v if v < Sats::_1K => 3,
|
||||
v if v < Sats::_10K => 4,
|
||||
v if v < Sats::_100K => 5,
|
||||
v if v < Sats::_1M => 6,
|
||||
v if v < Sats::_10M => 7,
|
||||
v if v < Sats::_1BTC => 8,
|
||||
v if v < Sats::_10BTC => 9,
|
||||
v if v < Sats::_100BTC => 10,
|
||||
v if v < Sats::_1K_BTC => 11,
|
||||
v if v < Sats::_10K_BTC => 12,
|
||||
v if v < Sats::_100K_BTC => 13,
|
||||
_ => 14,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if two amounts are in different buckets. O(1).
|
||||
#[inline(always)]
|
||||
pub fn amounts_in_different_buckets(a: Sats, b: Sats) -> bool {
|
||||
AmountBucket::from(a) != AmountBucket::from(b)
|
||||
}
|
||||
|
||||
/// Amount range bounds
|
||||
pub const AMOUNT_RANGE_BOUNDS: ByAmountRange<Range<Sats>> = ByAmountRange {
|
||||
_0sats: Sats::ZERO..Sats::_1,
|
||||
_1sat_to_10sats: Sats::_1..Sats::_10,
|
||||
_10sats_to_100sats: Sats::_10..Sats::_100,
|
||||
_100sats_to_1k_sats: Sats::_100..Sats::_1K,
|
||||
_1k_sats_to_10k_sats: Sats::_1K..Sats::_10K,
|
||||
_10k_sats_to_100k_sats: Sats::_10K..Sats::_100K,
|
||||
_100k_sats_to_1m_sats: Sats::_100K..Sats::_1M,
|
||||
_1m_sats_to_10m_sats: Sats::_1M..Sats::_10M,
|
||||
_10m_sats_to_1btc: Sats::_10M..Sats::_1BTC,
|
||||
_1btc_to_10btc: Sats::_1BTC..Sats::_10BTC,
|
||||
_10btc_to_100btc: Sats::_10BTC..Sats::_100BTC,
|
||||
_100btc_to_1k_btc: Sats::_100BTC..Sats::_1K_BTC,
|
||||
_1k_btc_to_10k_btc: Sats::_1K_BTC..Sats::_10K_BTC,
|
||||
_10k_btc_to_100k_btc: Sats::_10K_BTC..Sats::_100K_BTC,
|
||||
_100k_btc_or_more: Sats::_100K_BTC..Sats::MAX,
|
||||
};
|
||||
|
||||
/// Amount range names
|
||||
pub const AMOUNT_RANGE_NAMES: ByAmountRange<CohortName> = ByAmountRange {
|
||||
_0sats: CohortName::new("with_0sats", "0 sats", "0 Sats"),
|
||||
_1sat_to_10sats: CohortName::new("above_1sat_under_10sats", "1-10 sats", "1 to 10 Sats"),
|
||||
_10sats_to_100sats: CohortName::new(
|
||||
"above_10sats_under_100sats",
|
||||
"10-100 sats",
|
||||
"10 to 100 Sats",
|
||||
),
|
||||
_100sats_to_1k_sats: CohortName::new(
|
||||
"above_100sats_under_1k_sats",
|
||||
"100-1k sats",
|
||||
"100 to 1K Sats",
|
||||
),
|
||||
_1k_sats_to_10k_sats: CohortName::new(
|
||||
"above_1k_sats_under_10k_sats",
|
||||
"1k-10k sats",
|
||||
"1K to 10K Sats",
|
||||
),
|
||||
_10k_sats_to_100k_sats: CohortName::new(
|
||||
"above_10k_sats_under_100k_sats",
|
||||
"10k-100k sats",
|
||||
"10K to 100K Sats",
|
||||
),
|
||||
_100k_sats_to_1m_sats: CohortName::new(
|
||||
"above_100k_sats_under_1m_sats",
|
||||
"100k-1M sats",
|
||||
"100K to 1M Sats",
|
||||
),
|
||||
_1m_sats_to_10m_sats: CohortName::new(
|
||||
"above_1m_sats_under_10m_sats",
|
||||
"1M-10M sats",
|
||||
"1M to 10M Sats",
|
||||
),
|
||||
_10m_sats_to_1btc: CohortName::new("above_10m_sats_under_1btc", "0.1-1 BTC", "0.1 to 1 BTC"),
|
||||
_1btc_to_10btc: CohortName::new("above_1btc_under_10btc", "1-10 BTC", "1 to 10 BTC"),
|
||||
_10btc_to_100btc: CohortName::new("above_10btc_under_100btc", "10-100 BTC", "10 to 100 BTC"),
|
||||
_100btc_to_1k_btc: CohortName::new("above_100btc_under_1k_btc", "100-1k BTC", "100 to 1K BTC"),
|
||||
_1k_btc_to_10k_btc: CohortName::new(
|
||||
"above_1k_btc_under_10k_btc",
|
||||
"1k-10k BTC",
|
||||
"1K to 10K BTC",
|
||||
),
|
||||
_10k_btc_to_100k_btc: CohortName::new(
|
||||
"above_10k_btc_under_100k_btc",
|
||||
"10k-100k BTC",
|
||||
"10K to 100K BTC",
|
||||
),
|
||||
_100k_btc_or_more: CohortName::new("above_100k_btc", "100k+ BTC", "100K+ BTC"),
|
||||
};
|
||||
|
||||
/// Amount range filters
|
||||
pub const AMOUNT_RANGE_FILTERS: ByAmountRange<Filter> = ByAmountRange {
|
||||
_0sats: Filter::Amount(AmountFilter::Range(AMOUNT_RANGE_BOUNDS._0sats)),
|
||||
_1sat_to_10sats: Filter::Amount(AmountFilter::Range(AMOUNT_RANGE_BOUNDS._1sat_to_10sats)),
|
||||
_10sats_to_100sats: Filter::Amount(AmountFilter::Range(AMOUNT_RANGE_BOUNDS._10sats_to_100sats)),
|
||||
_100sats_to_1k_sats: Filter::Amount(AmountFilter::Range(
|
||||
AMOUNT_RANGE_BOUNDS._100sats_to_1k_sats,
|
||||
)),
|
||||
_1k_sats_to_10k_sats: Filter::Amount(AmountFilter::Range(
|
||||
AMOUNT_RANGE_BOUNDS._1k_sats_to_10k_sats,
|
||||
)),
|
||||
_10k_sats_to_100k_sats: Filter::Amount(AmountFilter::Range(
|
||||
AMOUNT_RANGE_BOUNDS._10k_sats_to_100k_sats,
|
||||
)),
|
||||
_100k_sats_to_1m_sats: Filter::Amount(AmountFilter::Range(
|
||||
AMOUNT_RANGE_BOUNDS._100k_sats_to_1m_sats,
|
||||
)),
|
||||
_1m_sats_to_10m_sats: Filter::Amount(AmountFilter::Range(
|
||||
AMOUNT_RANGE_BOUNDS._1m_sats_to_10m_sats,
|
||||
)),
|
||||
_10m_sats_to_1btc: Filter::Amount(AmountFilter::Range(AMOUNT_RANGE_BOUNDS._10m_sats_to_1btc)),
|
||||
_1btc_to_10btc: Filter::Amount(AmountFilter::Range(AMOUNT_RANGE_BOUNDS._1btc_to_10btc)),
|
||||
_10btc_to_100btc: Filter::Amount(AmountFilter::Range(AMOUNT_RANGE_BOUNDS._10btc_to_100btc)),
|
||||
_100btc_to_1k_btc: Filter::Amount(AmountFilter::Range(AMOUNT_RANGE_BOUNDS._100btc_to_1k_btc)),
|
||||
_1k_btc_to_10k_btc: Filter::Amount(AmountFilter::Range(AMOUNT_RANGE_BOUNDS._1k_btc_to_10k_btc)),
|
||||
_10k_btc_to_100k_btc: Filter::Amount(AmountFilter::Range(
|
||||
AMOUNT_RANGE_BOUNDS._10k_btc_to_100k_btc,
|
||||
)),
|
||||
_100k_btc_or_more: Filter::Amount(AmountFilter::Range(AMOUNT_RANGE_BOUNDS._100k_btc_or_more)),
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Clone, Traversable, Serialize)]
|
||||
pub struct ByAmountRange<T> {
|
||||
pub _0sats: T,
|
||||
pub _1sat_to_10sats: T,
|
||||
pub _10sats_to_100sats: T,
|
||||
pub _100sats_to_1k_sats: T,
|
||||
pub _1k_sats_to_10k_sats: T,
|
||||
pub _10k_sats_to_100k_sats: T,
|
||||
pub _100k_sats_to_1m_sats: T,
|
||||
pub _1m_sats_to_10m_sats: T,
|
||||
pub _10m_sats_to_1btc: T,
|
||||
pub _1btc_to_10btc: T,
|
||||
pub _10btc_to_100btc: T,
|
||||
pub _100btc_to_1k_btc: T,
|
||||
pub _1k_btc_to_10k_btc: T,
|
||||
pub _10k_btc_to_100k_btc: T,
|
||||
pub _100k_btc_or_more: T,
|
||||
}
|
||||
|
||||
impl ByAmountRange<CohortName> {
|
||||
pub const fn names() -> &'static Self {
|
||||
&AMOUNT_RANGE_NAMES
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ByAmountRange<T> {
|
||||
pub fn new<F>(mut create: F) -> Self
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> T,
|
||||
{
|
||||
let f = AMOUNT_RANGE_FILTERS;
|
||||
let n = AMOUNT_RANGE_NAMES;
|
||||
Self {
|
||||
_0sats: create(f._0sats.clone(), n._0sats.id),
|
||||
_1sat_to_10sats: create(f._1sat_to_10sats.clone(), n._1sat_to_10sats.id),
|
||||
_10sats_to_100sats: create(f._10sats_to_100sats.clone(), n._10sats_to_100sats.id),
|
||||
_100sats_to_1k_sats: create(f._100sats_to_1k_sats.clone(), n._100sats_to_1k_sats.id),
|
||||
_1k_sats_to_10k_sats: create(f._1k_sats_to_10k_sats.clone(), n._1k_sats_to_10k_sats.id),
|
||||
_10k_sats_to_100k_sats: create(
|
||||
f._10k_sats_to_100k_sats.clone(),
|
||||
n._10k_sats_to_100k_sats.id,
|
||||
),
|
||||
_100k_sats_to_1m_sats: create(
|
||||
f._100k_sats_to_1m_sats.clone(),
|
||||
n._100k_sats_to_1m_sats.id,
|
||||
),
|
||||
_1m_sats_to_10m_sats: create(f._1m_sats_to_10m_sats.clone(), n._1m_sats_to_10m_sats.id),
|
||||
_10m_sats_to_1btc: create(f._10m_sats_to_1btc.clone(), n._10m_sats_to_1btc.id),
|
||||
_1btc_to_10btc: create(f._1btc_to_10btc.clone(), n._1btc_to_10btc.id),
|
||||
_10btc_to_100btc: create(f._10btc_to_100btc.clone(), n._10btc_to_100btc.id),
|
||||
_100btc_to_1k_btc: create(f._100btc_to_1k_btc.clone(), n._100btc_to_1k_btc.id),
|
||||
_1k_btc_to_10k_btc: create(f._1k_btc_to_10k_btc.clone(), n._1k_btc_to_10k_btc.id),
|
||||
_10k_btc_to_100k_btc: create(f._10k_btc_to_100k_btc.clone(), n._10k_btc_to_100k_btc.id),
|
||||
_100k_btc_or_more: create(f._100k_btc_or_more.clone(), n._100k_btc_or_more.id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_new<F, E>(mut create: F) -> Result<Self, E>
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> Result<T, E>,
|
||||
{
|
||||
let f = AMOUNT_RANGE_FILTERS;
|
||||
let n = AMOUNT_RANGE_NAMES;
|
||||
Ok(Self {
|
||||
_0sats: create(f._0sats.clone(), n._0sats.id)?,
|
||||
_1sat_to_10sats: create(f._1sat_to_10sats.clone(), n._1sat_to_10sats.id)?,
|
||||
_10sats_to_100sats: create(f._10sats_to_100sats.clone(), n._10sats_to_100sats.id)?,
|
||||
_100sats_to_1k_sats: create(f._100sats_to_1k_sats.clone(), n._100sats_to_1k_sats.id)?,
|
||||
_1k_sats_to_10k_sats: create(
|
||||
f._1k_sats_to_10k_sats.clone(),
|
||||
n._1k_sats_to_10k_sats.id,
|
||||
)?,
|
||||
_10k_sats_to_100k_sats: create(
|
||||
f._10k_sats_to_100k_sats.clone(),
|
||||
n._10k_sats_to_100k_sats.id,
|
||||
)?,
|
||||
_100k_sats_to_1m_sats: create(
|
||||
f._100k_sats_to_1m_sats.clone(),
|
||||
n._100k_sats_to_1m_sats.id,
|
||||
)?,
|
||||
_1m_sats_to_10m_sats: create(
|
||||
f._1m_sats_to_10m_sats.clone(),
|
||||
n._1m_sats_to_10m_sats.id,
|
||||
)?,
|
||||
_10m_sats_to_1btc: create(f._10m_sats_to_1btc.clone(), n._10m_sats_to_1btc.id)?,
|
||||
_1btc_to_10btc: create(f._1btc_to_10btc.clone(), n._1btc_to_10btc.id)?,
|
||||
_10btc_to_100btc: create(f._10btc_to_100btc.clone(), n._10btc_to_100btc.id)?,
|
||||
_100btc_to_1k_btc: create(f._100btc_to_1k_btc.clone(), n._100btc_to_1k_btc.id)?,
|
||||
_1k_btc_to_10k_btc: create(f._1k_btc_to_10k_btc.clone(), n._1k_btc_to_10k_btc.id)?,
|
||||
_10k_btc_to_100k_btc: create(
|
||||
f._10k_btc_to_100k_btc.clone(),
|
||||
n._10k_btc_to_100k_btc.id,
|
||||
)?,
|
||||
_100k_btc_or_more: create(f._100k_btc_or_more.clone(), n._100k_btc_or_more.id)?,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get(&self, value: Sats) -> &T {
|
||||
match AmountBucket::from(value).0 {
|
||||
0 => &self._0sats,
|
||||
1 => &self._1sat_to_10sats,
|
||||
2 => &self._10sats_to_100sats,
|
||||
3 => &self._100sats_to_1k_sats,
|
||||
4 => &self._1k_sats_to_10k_sats,
|
||||
5 => &self._10k_sats_to_100k_sats,
|
||||
6 => &self._100k_sats_to_1m_sats,
|
||||
7 => &self._1m_sats_to_10m_sats,
|
||||
8 => &self._10m_sats_to_1btc,
|
||||
9 => &self._1btc_to_10btc,
|
||||
10 => &self._10btc_to_100btc,
|
||||
11 => &self._100btc_to_1k_btc,
|
||||
12 => &self._1k_btc_to_10k_btc,
|
||||
13 => &self._10k_btc_to_100k_btc,
|
||||
_ => &self._100k_btc_or_more,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get_mut(&mut self, value: Sats) -> &mut T {
|
||||
self.get_mut_by_bucket(AmountBucket::from(value))
|
||||
}
|
||||
|
||||
/// Get mutable reference by pre-computed bucket index.
|
||||
/// Use with `AmountBucket::transition_to` to avoid recomputing bucket.
|
||||
#[inline(always)]
|
||||
pub fn get_mut_by_bucket(&mut self, bucket: AmountBucket) -> &mut T {
|
||||
match bucket.0 {
|
||||
0 => &mut self._0sats,
|
||||
1 => &mut self._1sat_to_10sats,
|
||||
2 => &mut self._10sats_to_100sats,
|
||||
3 => &mut self._100sats_to_1k_sats,
|
||||
4 => &mut self._1k_sats_to_10k_sats,
|
||||
5 => &mut self._10k_sats_to_100k_sats,
|
||||
6 => &mut self._100k_sats_to_1m_sats,
|
||||
7 => &mut self._1m_sats_to_10m_sats,
|
||||
8 => &mut self._10m_sats_to_1btc,
|
||||
9 => &mut self._1btc_to_10btc,
|
||||
10 => &mut self._10btc_to_100btc,
|
||||
11 => &mut self._100btc_to_1k_btc,
|
||||
12 => &mut self._1k_btc_to_10k_btc,
|
||||
13 => &mut self._10k_btc_to_100k_btc,
|
||||
_ => &mut self._100k_btc_or_more,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
[
|
||||
&self._0sats,
|
||||
&self._1sat_to_10sats,
|
||||
&self._10sats_to_100sats,
|
||||
&self._100sats_to_1k_sats,
|
||||
&self._1k_sats_to_10k_sats,
|
||||
&self._10k_sats_to_100k_sats,
|
||||
&self._100k_sats_to_1m_sats,
|
||||
&self._1m_sats_to_10m_sats,
|
||||
&self._10m_sats_to_1btc,
|
||||
&self._1btc_to_10btc,
|
||||
&self._10btc_to_100btc,
|
||||
&self._100btc_to_1k_btc,
|
||||
&self._1k_btc_to_10k_btc,
|
||||
&self._10k_btc_to_100k_btc,
|
||||
&self._100k_btc_or_more,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn iter_typed(&self) -> impl Iterator<Item = (Sats, &T)> {
|
||||
[
|
||||
(Sats::ZERO, &self._0sats),
|
||||
(Sats::_1, &self._1sat_to_10sats),
|
||||
(Sats::_10, &self._10sats_to_100sats),
|
||||
(Sats::_100, &self._100sats_to_1k_sats),
|
||||
(Sats::_1K, &self._1k_sats_to_10k_sats),
|
||||
(Sats::_10K, &self._10k_sats_to_100k_sats),
|
||||
(Sats::_100K, &self._100k_sats_to_1m_sats),
|
||||
(Sats::_1M, &self._1m_sats_to_10m_sats),
|
||||
(Sats::_10M, &self._10m_sats_to_1btc),
|
||||
(Sats::_1BTC, &self._1btc_to_10btc),
|
||||
(Sats::_10BTC, &self._10btc_to_100btc),
|
||||
(Sats::_100BTC, &self._100btc_to_1k_btc),
|
||||
(Sats::_1K_BTC, &self._1k_btc_to_10k_btc),
|
||||
(Sats::_10K_BTC, &self._10k_btc_to_100k_btc),
|
||||
(Sats::_100K_BTC, &self._100k_btc_or_more),
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
[
|
||||
&mut self._0sats,
|
||||
&mut self._1sat_to_10sats,
|
||||
&mut self._10sats_to_100sats,
|
||||
&mut self._100sats_to_1k_sats,
|
||||
&mut self._1k_sats_to_10k_sats,
|
||||
&mut self._10k_sats_to_100k_sats,
|
||||
&mut self._100k_sats_to_1m_sats,
|
||||
&mut self._1m_sats_to_10m_sats,
|
||||
&mut self._10m_sats_to_1btc,
|
||||
&mut self._1btc_to_10btc,
|
||||
&mut self._10btc_to_100btc,
|
||||
&mut self._100btc_to_1k_btc,
|
||||
&mut self._1k_btc_to_10k_btc,
|
||||
&mut self._10k_btc_to_100k_btc,
|
||||
&mut self._100k_btc_or_more,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
[
|
||||
&mut self._0sats,
|
||||
&mut self._1sat_to_10sats,
|
||||
&mut self._10sats_to_100sats,
|
||||
&mut self._100sats_to_1k_sats,
|
||||
&mut self._1k_sats_to_10k_sats,
|
||||
&mut self._10k_sats_to_100k_sats,
|
||||
&mut self._100k_sats_to_1m_sats,
|
||||
&mut self._1m_sats_to_10m_sats,
|
||||
&mut self._10m_sats_to_1btc,
|
||||
&mut self._1btc_to_10btc,
|
||||
&mut self._10btc_to_100btc,
|
||||
&mut self._100btc_to_1k_btc,
|
||||
&mut self._1k_btc_to_10k_btc,
|
||||
&mut self._10k_btc_to_100k_btc,
|
||||
&mut self._100k_btc_or_more,
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Add for ByAmountRange<T>
|
||||
where
|
||||
T: Add<Output = T>,
|
||||
{
|
||||
type Output = Self;
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self {
|
||||
_0sats: self._0sats + rhs._0sats,
|
||||
_1sat_to_10sats: self._1sat_to_10sats + rhs._1sat_to_10sats,
|
||||
_10sats_to_100sats: self._10sats_to_100sats + rhs._10sats_to_100sats,
|
||||
_100sats_to_1k_sats: self._100sats_to_1k_sats + rhs._100sats_to_1k_sats,
|
||||
_1k_sats_to_10k_sats: self._1k_sats_to_10k_sats + rhs._1k_sats_to_10k_sats,
|
||||
_10k_sats_to_100k_sats: self._10k_sats_to_100k_sats + rhs._10k_sats_to_100k_sats,
|
||||
_100k_sats_to_1m_sats: self._100k_sats_to_1m_sats + rhs._100k_sats_to_1m_sats,
|
||||
_1m_sats_to_10m_sats: self._1m_sats_to_10m_sats + rhs._1m_sats_to_10m_sats,
|
||||
_10m_sats_to_1btc: self._10m_sats_to_1btc + rhs._10m_sats_to_1btc,
|
||||
_1btc_to_10btc: self._1btc_to_10btc + rhs._1btc_to_10btc,
|
||||
_10btc_to_100btc: self._10btc_to_100btc + rhs._10btc_to_100btc,
|
||||
_100btc_to_1k_btc: self._100btc_to_1k_btc + rhs._100btc_to_1k_btc,
|
||||
_1k_btc_to_10k_btc: self._1k_btc_to_10k_btc + rhs._1k_btc_to_10k_btc,
|
||||
_10k_btc_to_100k_btc: self._10k_btc_to_100k_btc + rhs._10k_btc_to_100k_btc,
|
||||
_100k_btc_or_more: self._100k_btc_or_more + rhs._100k_btc_or_more,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AddAssign for ByAmountRange<T>
|
||||
where
|
||||
T: AddAssign,
|
||||
{
|
||||
fn add_assign(&mut self, rhs: Self) {
|
||||
self._0sats += rhs._0sats;
|
||||
self._1sat_to_10sats += rhs._1sat_to_10sats;
|
||||
self._10sats_to_100sats += rhs._10sats_to_100sats;
|
||||
self._100sats_to_1k_sats += rhs._100sats_to_1k_sats;
|
||||
self._1k_sats_to_10k_sats += rhs._1k_sats_to_10k_sats;
|
||||
self._10k_sats_to_100k_sats += rhs._10k_sats_to_100k_sats;
|
||||
self._100k_sats_to_1m_sats += rhs._100k_sats_to_1m_sats;
|
||||
self._1m_sats_to_10m_sats += rhs._1m_sats_to_10m_sats;
|
||||
self._10m_sats_to_1btc += rhs._10m_sats_to_1btc;
|
||||
self._1btc_to_10btc += rhs._1btc_to_10btc;
|
||||
self._10btc_to_100btc += rhs._10btc_to_100btc;
|
||||
self._100btc_to_1k_btc += rhs._100btc_to_1k_btc;
|
||||
self._1k_btc_to_10k_btc += rhs._1k_btc_to_10k_btc;
|
||||
self._10k_btc_to_100k_btc += rhs._10k_btc_to_100k_btc;
|
||||
self._100k_btc_or_more += rhs._100k_btc_or_more;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
use brk_traversable::Traversable;
|
||||
|
||||
#[derive(Debug, Default, Traversable)]
|
||||
pub struct ByAnyAddr<T> {
|
||||
pub funded: T,
|
||||
pub empty: T,
|
||||
}
|
||||
|
||||
impl<T> ByAnyAddr<Option<T>> {
|
||||
pub fn take(&mut self) {
|
||||
self.funded.take();
|
||||
self.empty.take();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
use brk_traversable::Traversable;
|
||||
|
||||
#[derive(Debug, Default, Traversable)]
|
||||
pub struct ByAnyAddress<T> {
|
||||
pub loaded: T,
|
||||
pub empty: T,
|
||||
}
|
||||
|
||||
impl<T> ByAnyAddress<Option<T>> {
|
||||
pub fn take(&mut self) {
|
||||
self.loaded.take();
|
||||
self.empty.take();
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{HalvingEpoch, Height};
|
||||
use brk_types::{Halving, Height};
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use serde::Serialize;
|
||||
|
||||
use super::{CohortName, Filter};
|
||||
|
||||
/// Epoch values
|
||||
pub const EPOCH_VALUES: ByEpoch<HalvingEpoch> = ByEpoch {
|
||||
_0: HalvingEpoch::new(0),
|
||||
_1: HalvingEpoch::new(1),
|
||||
_2: HalvingEpoch::new(2),
|
||||
_3: HalvingEpoch::new(3),
|
||||
_4: HalvingEpoch::new(4),
|
||||
pub const EPOCH_VALUES: ByEpoch<Halving> = ByEpoch {
|
||||
_0: Halving::new(0),
|
||||
_1: Halving::new(1),
|
||||
_2: Halving::new(2),
|
||||
_3: Halving::new(3),
|
||||
_4: Halving::new(4),
|
||||
};
|
||||
|
||||
/// Epoch filters
|
||||
@@ -25,11 +25,11 @@ pub const EPOCH_FILTERS: ByEpoch<Filter> = ByEpoch {
|
||||
|
||||
/// Epoch names
|
||||
pub const EPOCH_NAMES: ByEpoch<CohortName> = ByEpoch {
|
||||
_0: CohortName::new("epoch_0", "Epoch 0", "Epoch 0"),
|
||||
_1: CohortName::new("epoch_1", "Epoch 1", "Epoch 1"),
|
||||
_2: CohortName::new("epoch_2", "Epoch 2", "Epoch 2"),
|
||||
_3: CohortName::new("epoch_3", "Epoch 3", "Epoch 3"),
|
||||
_4: CohortName::new("epoch_4", "Epoch 4", "Epoch 4"),
|
||||
_0: CohortName::new("epoch_0", "0", "Epoch 0"),
|
||||
_1: CohortName::new("epoch_1", "1", "Epoch 1"),
|
||||
_2: CohortName::new("epoch_2", "2", "Epoch 2"),
|
||||
_3: CohortName::new("epoch_3", "3", "Epoch 3"),
|
||||
_4: CohortName::new("epoch_4", "4", "Epoch 4"),
|
||||
};
|
||||
|
||||
#[derive(Default, Clone, Traversable, Serialize)]
|
||||
@@ -107,20 +107,20 @@ impl<T> ByEpoch<T> {
|
||||
.into_par_iter()
|
||||
}
|
||||
|
||||
pub fn mut_vec_from_height(&mut self, height: Height) -> &mut T {
|
||||
let epoch = HalvingEpoch::from(height);
|
||||
if epoch == HalvingEpoch::new(0) {
|
||||
&mut self._0
|
||||
} else if epoch == HalvingEpoch::new(1) {
|
||||
&mut self._1
|
||||
} else if epoch == HalvingEpoch::new(2) {
|
||||
&mut self._2
|
||||
} else if epoch == HalvingEpoch::new(3) {
|
||||
&mut self._3
|
||||
} else if epoch == HalvingEpoch::new(4) {
|
||||
&mut self._4
|
||||
pub fn mut_vec_from_height(&mut self, height: Height) -> Option<&mut T> {
|
||||
let epoch = Halving::from(height);
|
||||
if epoch == Halving::new(0) {
|
||||
Some(&mut self._0)
|
||||
} else if epoch == Halving::new(1) {
|
||||
Some(&mut self._1)
|
||||
} else if epoch == Halving::new(2) {
|
||||
Some(&mut self._2)
|
||||
} else if epoch == Halving::new(3) {
|
||||
Some(&mut self._3)
|
||||
} else if epoch == Halving::new(4) {
|
||||
Some(&mut self._4)
|
||||
} else {
|
||||
todo!("")
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::Sats;
|
||||
use rayon::prelude::*;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::{AmountFilter, CohortName, Filter};
|
||||
|
||||
/// Greater-or-equal amount thresholds
|
||||
pub const GE_AMOUNT_THRESHOLDS: ByGreatEqualAmount<Sats> = ByGreatEqualAmount {
|
||||
_1sat: Sats::_1,
|
||||
_10sats: Sats::_10,
|
||||
_100sats: Sats::_100,
|
||||
_1k_sats: Sats::_1K,
|
||||
_10k_sats: Sats::_10K,
|
||||
_100k_sats: Sats::_100K,
|
||||
_1m_sats: Sats::_1M,
|
||||
_10m_sats: Sats::_10M,
|
||||
_1btc: Sats::_1BTC,
|
||||
_10btc: Sats::_10BTC,
|
||||
_100btc: Sats::_100BTC,
|
||||
_1k_btc: Sats::_1K_BTC,
|
||||
_10k_btc: Sats::_10K_BTC,
|
||||
};
|
||||
|
||||
/// Greater-or-equal amount names
|
||||
pub const GE_AMOUNT_NAMES: ByGreatEqualAmount<CohortName> = ByGreatEqualAmount {
|
||||
_1sat: CohortName::new("above_1sat", "1+ sats", "Above 1 Sat"),
|
||||
_10sats: CohortName::new("above_10sats", "10+ sats", "Above 10 Sats"),
|
||||
_100sats: CohortName::new("above_100sats", "100+ sats", "Above 100 Sats"),
|
||||
_1k_sats: CohortName::new("above_1k_sats", "1k+ sats", "Above 1K Sats"),
|
||||
_10k_sats: CohortName::new("above_10k_sats", "10k+ sats", "Above 10K Sats"),
|
||||
_100k_sats: CohortName::new("above_100k_sats", "100k+ sats", "Above 100K Sats"),
|
||||
_1m_sats: CohortName::new("above_1m_sats", "1M+ sats", "Above 1M Sats"),
|
||||
_10m_sats: CohortName::new("above_10m_sats", "0.1+ BTC", "Above 0.1 BTC"),
|
||||
_1btc: CohortName::new("above_1btc", "1+ BTC", "Above 1 BTC"),
|
||||
_10btc: CohortName::new("above_10btc", "10+ BTC", "Above 10 BTC"),
|
||||
_100btc: CohortName::new("above_100btc", "100+ BTC", "Above 100 BTC"),
|
||||
_1k_btc: CohortName::new("above_1k_btc", "1k+ BTC", "Above 1K BTC"),
|
||||
_10k_btc: CohortName::new("above_10k_btc", "10k+ BTC", "Above 10K BTC"),
|
||||
};
|
||||
|
||||
/// Greater-or-equal amount filters
|
||||
pub const GE_AMOUNT_FILTERS: ByGreatEqualAmount<Filter> = ByGreatEqualAmount {
|
||||
_1sat: Filter::Amount(AmountFilter::GreaterOrEqual(GE_AMOUNT_THRESHOLDS._1sat)),
|
||||
_10sats: Filter::Amount(AmountFilter::GreaterOrEqual(GE_AMOUNT_THRESHOLDS._10sats)),
|
||||
_100sats: Filter::Amount(AmountFilter::GreaterOrEqual(GE_AMOUNT_THRESHOLDS._100sats)),
|
||||
_1k_sats: Filter::Amount(AmountFilter::GreaterOrEqual(GE_AMOUNT_THRESHOLDS._1k_sats)),
|
||||
_10k_sats: Filter::Amount(AmountFilter::GreaterOrEqual(GE_AMOUNT_THRESHOLDS._10k_sats)),
|
||||
_100k_sats: Filter::Amount(AmountFilter::GreaterOrEqual(
|
||||
GE_AMOUNT_THRESHOLDS._100k_sats,
|
||||
)),
|
||||
_1m_sats: Filter::Amount(AmountFilter::GreaterOrEqual(GE_AMOUNT_THRESHOLDS._1m_sats)),
|
||||
_10m_sats: Filter::Amount(AmountFilter::GreaterOrEqual(GE_AMOUNT_THRESHOLDS._10m_sats)),
|
||||
_1btc: Filter::Amount(AmountFilter::GreaterOrEqual(GE_AMOUNT_THRESHOLDS._1btc)),
|
||||
_10btc: Filter::Amount(AmountFilter::GreaterOrEqual(GE_AMOUNT_THRESHOLDS._10btc)),
|
||||
_100btc: Filter::Amount(AmountFilter::GreaterOrEqual(GE_AMOUNT_THRESHOLDS._100btc)),
|
||||
_1k_btc: Filter::Amount(AmountFilter::GreaterOrEqual(GE_AMOUNT_THRESHOLDS._1k_btc)),
|
||||
_10k_btc: Filter::Amount(AmountFilter::GreaterOrEqual(GE_AMOUNT_THRESHOLDS._10k_btc)),
|
||||
};
|
||||
|
||||
#[derive(Default, Clone, Traversable, Serialize)]
|
||||
pub struct ByGreatEqualAmount<T> {
|
||||
pub _1sat: T,
|
||||
pub _10sats: T,
|
||||
pub _100sats: T,
|
||||
pub _1k_sats: T,
|
||||
pub _10k_sats: T,
|
||||
pub _100k_sats: T,
|
||||
pub _1m_sats: T,
|
||||
pub _10m_sats: T,
|
||||
pub _1btc: T,
|
||||
pub _10btc: T,
|
||||
pub _100btc: T,
|
||||
pub _1k_btc: T,
|
||||
pub _10k_btc: T,
|
||||
}
|
||||
|
||||
impl ByGreatEqualAmount<CohortName> {
|
||||
pub const fn names() -> &'static Self {
|
||||
&GE_AMOUNT_NAMES
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ByGreatEqualAmount<T> {
|
||||
pub fn new<F>(mut create: F) -> Self
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> T,
|
||||
{
|
||||
let f = GE_AMOUNT_FILTERS;
|
||||
let n = GE_AMOUNT_NAMES;
|
||||
Self {
|
||||
_1sat: create(f._1sat.clone(), n._1sat.id),
|
||||
_10sats: create(f._10sats.clone(), n._10sats.id),
|
||||
_100sats: create(f._100sats.clone(), n._100sats.id),
|
||||
_1k_sats: create(f._1k_sats.clone(), n._1k_sats.id),
|
||||
_10k_sats: create(f._10k_sats.clone(), n._10k_sats.id),
|
||||
_100k_sats: create(f._100k_sats.clone(), n._100k_sats.id),
|
||||
_1m_sats: create(f._1m_sats.clone(), n._1m_sats.id),
|
||||
_10m_sats: create(f._10m_sats.clone(), n._10m_sats.id),
|
||||
_1btc: create(f._1btc.clone(), n._1btc.id),
|
||||
_10btc: create(f._10btc.clone(), n._10btc.id),
|
||||
_100btc: create(f._100btc.clone(), n._100btc.id),
|
||||
_1k_btc: create(f._1k_btc.clone(), n._1k_btc.id),
|
||||
_10k_btc: create(f._10k_btc.clone(), n._10k_btc.id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_new<F, E>(mut create: F) -> Result<Self, E>
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> Result<T, E>,
|
||||
{
|
||||
let f = GE_AMOUNT_FILTERS;
|
||||
let n = GE_AMOUNT_NAMES;
|
||||
Ok(Self {
|
||||
_1sat: create(f._1sat.clone(), n._1sat.id)?,
|
||||
_10sats: create(f._10sats.clone(), n._10sats.id)?,
|
||||
_100sats: create(f._100sats.clone(), n._100sats.id)?,
|
||||
_1k_sats: create(f._1k_sats.clone(), n._1k_sats.id)?,
|
||||
_10k_sats: create(f._10k_sats.clone(), n._10k_sats.id)?,
|
||||
_100k_sats: create(f._100k_sats.clone(), n._100k_sats.id)?,
|
||||
_1m_sats: create(f._1m_sats.clone(), n._1m_sats.id)?,
|
||||
_10m_sats: create(f._10m_sats.clone(), n._10m_sats.id)?,
|
||||
_1btc: create(f._1btc.clone(), n._1btc.id)?,
|
||||
_10btc: create(f._10btc.clone(), n._10btc.id)?,
|
||||
_100btc: create(f._100btc.clone(), n._100btc.id)?,
|
||||
_1k_btc: create(f._1k_btc.clone(), n._1k_btc.id)?,
|
||||
_10k_btc: create(f._10k_btc.clone(), n._10k_btc.id)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
[
|
||||
&self._1sat,
|
||||
&self._10sats,
|
||||
&self._100sats,
|
||||
&self._1k_sats,
|
||||
&self._10k_sats,
|
||||
&self._100k_sats,
|
||||
&self._1m_sats,
|
||||
&self._10m_sats,
|
||||
&self._1btc,
|
||||
&self._10btc,
|
||||
&self._100btc,
|
||||
&self._1k_btc,
|
||||
&self._10k_btc,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
[
|
||||
&mut self._1sat,
|
||||
&mut self._10sats,
|
||||
&mut self._100sats,
|
||||
&mut self._1k_sats,
|
||||
&mut self._10k_sats,
|
||||
&mut self._100k_sats,
|
||||
&mut self._1m_sats,
|
||||
&mut self._10m_sats,
|
||||
&mut self._1btc,
|
||||
&mut self._10btc,
|
||||
&mut self._100btc,
|
||||
&mut self._1k_btc,
|
||||
&mut self._10k_btc,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
[
|
||||
&mut self._1sat,
|
||||
&mut self._10sats,
|
||||
&mut self._100sats,
|
||||
&mut self._1k_sats,
|
||||
&mut self._10k_sats,
|
||||
&mut self._100k_sats,
|
||||
&mut self._1m_sats,
|
||||
&mut self._10m_sats,
|
||||
&mut self._1btc,
|
||||
&mut self._10btc,
|
||||
&mut self._100btc,
|
||||
&mut self._1k_btc,
|
||||
&mut self._10k_btc,
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::Sats;
|
||||
use rayon::prelude::*;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::{AmountFilter, CohortName, Filter};
|
||||
|
||||
/// Lower-than amount thresholds
|
||||
pub const LT_AMOUNT_THRESHOLDS: ByLowerThanAmount<Sats> = ByLowerThanAmount {
|
||||
_10sats: Sats::_10,
|
||||
_100sats: Sats::_100,
|
||||
_1k_sats: Sats::_1K,
|
||||
_10k_sats: Sats::_10K,
|
||||
_100k_sats: Sats::_100K,
|
||||
_1m_sats: Sats::_1M,
|
||||
_10m_sats: Sats::_10M,
|
||||
_1btc: Sats::_1BTC,
|
||||
_10btc: Sats::_10BTC,
|
||||
_100btc: Sats::_100BTC,
|
||||
_1k_btc: Sats::_1K_BTC,
|
||||
_10k_btc: Sats::_10K_BTC,
|
||||
_100k_btc: Sats::_100K_BTC,
|
||||
};
|
||||
|
||||
/// Lower-than amount names
|
||||
pub const LT_AMOUNT_NAMES: ByLowerThanAmount<CohortName> = ByLowerThanAmount {
|
||||
_10sats: CohortName::new("under_10sats", "<10 sats", "Under 10 Sats"),
|
||||
_100sats: CohortName::new("under_100sats", "<100 sats", "Under 100 Sats"),
|
||||
_1k_sats: CohortName::new("under_1k_sats", "<1k sats", "Under 1K Sats"),
|
||||
_10k_sats: CohortName::new("under_10k_sats", "<10k sats", "Under 10K Sats"),
|
||||
_100k_sats: CohortName::new("under_100k_sats", "<100k sats", "Under 100K Sats"),
|
||||
_1m_sats: CohortName::new("under_1m_sats", "<1M sats", "Under 1M Sats"),
|
||||
_10m_sats: CohortName::new("under_10m_sats", "<0.1 BTC", "Under 0.1 BTC"),
|
||||
_1btc: CohortName::new("under_1btc", "<1 BTC", "Under 1 BTC"),
|
||||
_10btc: CohortName::new("under_10btc", "<10 BTC", "Under 10 BTC"),
|
||||
_100btc: CohortName::new("under_100btc", "<100 BTC", "Under 100 BTC"),
|
||||
_1k_btc: CohortName::new("under_1k_btc", "<1k BTC", "Under 1K BTC"),
|
||||
_10k_btc: CohortName::new("under_10k_btc", "<10k BTC", "Under 10K BTC"),
|
||||
_100k_btc: CohortName::new("under_100k_btc", "<100k BTC", "Under 100K BTC"),
|
||||
};
|
||||
|
||||
/// Lower-than amount filters
|
||||
pub const LT_AMOUNT_FILTERS: ByLowerThanAmount<Filter> = ByLowerThanAmount {
|
||||
_10sats: Filter::Amount(AmountFilter::LowerThan(LT_AMOUNT_THRESHOLDS._10sats)),
|
||||
_100sats: Filter::Amount(AmountFilter::LowerThan(LT_AMOUNT_THRESHOLDS._100sats)),
|
||||
_1k_sats: Filter::Amount(AmountFilter::LowerThan(LT_AMOUNT_THRESHOLDS._1k_sats)),
|
||||
_10k_sats: Filter::Amount(AmountFilter::LowerThan(LT_AMOUNT_THRESHOLDS._10k_sats)),
|
||||
_100k_sats: Filter::Amount(AmountFilter::LowerThan(LT_AMOUNT_THRESHOLDS._100k_sats)),
|
||||
_1m_sats: Filter::Amount(AmountFilter::LowerThan(LT_AMOUNT_THRESHOLDS._1m_sats)),
|
||||
_10m_sats: Filter::Amount(AmountFilter::LowerThan(LT_AMOUNT_THRESHOLDS._10m_sats)),
|
||||
_1btc: Filter::Amount(AmountFilter::LowerThan(LT_AMOUNT_THRESHOLDS._1btc)),
|
||||
_10btc: Filter::Amount(AmountFilter::LowerThan(LT_AMOUNT_THRESHOLDS._10btc)),
|
||||
_100btc: Filter::Amount(AmountFilter::LowerThan(LT_AMOUNT_THRESHOLDS._100btc)),
|
||||
_1k_btc: Filter::Amount(AmountFilter::LowerThan(LT_AMOUNT_THRESHOLDS._1k_btc)),
|
||||
_10k_btc: Filter::Amount(AmountFilter::LowerThan(LT_AMOUNT_THRESHOLDS._10k_btc)),
|
||||
_100k_btc: Filter::Amount(AmountFilter::LowerThan(LT_AMOUNT_THRESHOLDS._100k_btc)),
|
||||
};
|
||||
|
||||
#[derive(Default, Clone, Traversable, Serialize)]
|
||||
pub struct ByLowerThanAmount<T> {
|
||||
pub _10sats: T,
|
||||
pub _100sats: T,
|
||||
pub _1k_sats: T,
|
||||
pub _10k_sats: T,
|
||||
pub _100k_sats: T,
|
||||
pub _1m_sats: T,
|
||||
pub _10m_sats: T,
|
||||
pub _1btc: T,
|
||||
pub _10btc: T,
|
||||
pub _100btc: T,
|
||||
pub _1k_btc: T,
|
||||
pub _10k_btc: T,
|
||||
pub _100k_btc: T,
|
||||
}
|
||||
|
||||
impl ByLowerThanAmount<CohortName> {
|
||||
pub const fn names() -> &'static Self {
|
||||
<_AMOUNT_NAMES
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ByLowerThanAmount<T> {
|
||||
pub fn new<F>(mut create: F) -> Self
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> T,
|
||||
{
|
||||
let f = LT_AMOUNT_FILTERS;
|
||||
let n = LT_AMOUNT_NAMES;
|
||||
Self {
|
||||
_10sats: create(f._10sats.clone(), n._10sats.id),
|
||||
_100sats: create(f._100sats.clone(), n._100sats.id),
|
||||
_1k_sats: create(f._1k_sats.clone(), n._1k_sats.id),
|
||||
_10k_sats: create(f._10k_sats.clone(), n._10k_sats.id),
|
||||
_100k_sats: create(f._100k_sats.clone(), n._100k_sats.id),
|
||||
_1m_sats: create(f._1m_sats.clone(), n._1m_sats.id),
|
||||
_10m_sats: create(f._10m_sats.clone(), n._10m_sats.id),
|
||||
_1btc: create(f._1btc.clone(), n._1btc.id),
|
||||
_10btc: create(f._10btc.clone(), n._10btc.id),
|
||||
_100btc: create(f._100btc.clone(), n._100btc.id),
|
||||
_1k_btc: create(f._1k_btc.clone(), n._1k_btc.id),
|
||||
_10k_btc: create(f._10k_btc.clone(), n._10k_btc.id),
|
||||
_100k_btc: create(f._100k_btc.clone(), n._100k_btc.id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_new<F, E>(mut create: F) -> Result<Self, E>
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> Result<T, E>,
|
||||
{
|
||||
let f = LT_AMOUNT_FILTERS;
|
||||
let n = LT_AMOUNT_NAMES;
|
||||
Ok(Self {
|
||||
_10sats: create(f._10sats.clone(), n._10sats.id)?,
|
||||
_100sats: create(f._100sats.clone(), n._100sats.id)?,
|
||||
_1k_sats: create(f._1k_sats.clone(), n._1k_sats.id)?,
|
||||
_10k_sats: create(f._10k_sats.clone(), n._10k_sats.id)?,
|
||||
_100k_sats: create(f._100k_sats.clone(), n._100k_sats.id)?,
|
||||
_1m_sats: create(f._1m_sats.clone(), n._1m_sats.id)?,
|
||||
_10m_sats: create(f._10m_sats.clone(), n._10m_sats.id)?,
|
||||
_1btc: create(f._1btc.clone(), n._1btc.id)?,
|
||||
_10btc: create(f._10btc.clone(), n._10btc.id)?,
|
||||
_100btc: create(f._100btc.clone(), n._100btc.id)?,
|
||||
_1k_btc: create(f._1k_btc.clone(), n._1k_btc.id)?,
|
||||
_10k_btc: create(f._10k_btc.clone(), n._10k_btc.id)?,
|
||||
_100k_btc: create(f._100k_btc.clone(), n._100k_btc.id)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
[
|
||||
&self._10sats,
|
||||
&self._100sats,
|
||||
&self._1k_sats,
|
||||
&self._10k_sats,
|
||||
&self._100k_sats,
|
||||
&self._1m_sats,
|
||||
&self._10m_sats,
|
||||
&self._1btc,
|
||||
&self._10btc,
|
||||
&self._100btc,
|
||||
&self._1k_btc,
|
||||
&self._10k_btc,
|
||||
&self._100k_btc,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
[
|
||||
&mut self._10sats,
|
||||
&mut self._100sats,
|
||||
&mut self._1k_sats,
|
||||
&mut self._10k_sats,
|
||||
&mut self._100k_sats,
|
||||
&mut self._1m_sats,
|
||||
&mut self._10m_sats,
|
||||
&mut self._1btc,
|
||||
&mut self._10btc,
|
||||
&mut self._100btc,
|
||||
&mut self._1k_btc,
|
||||
&mut self._10k_btc,
|
||||
&mut self._100k_btc,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
[
|
||||
&mut self._10sats,
|
||||
&mut self._100sats,
|
||||
&mut self._1k_sats,
|
||||
&mut self._10k_sats,
|
||||
&mut self._100k_sats,
|
||||
&mut self._1m_sats,
|
||||
&mut self._10m_sats,
|
||||
&mut self._1btc,
|
||||
&mut self._10btc,
|
||||
&mut self._100btc,
|
||||
&mut self._1k_btc,
|
||||
&mut self._10k_btc,
|
||||
&mut self._100k_btc,
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
use brk_traversable::Traversable;
|
||||
use rayon::prelude::*;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::{
|
||||
CohortName, Filter, TimeFilter, HOURS_10Y, HOURS_12Y, HOURS_15Y, HOURS_1M, HOURS_1W, HOURS_1Y,
|
||||
HOURS_2M, HOURS_2Y, HOURS_3M, HOURS_3Y, HOURS_4M, HOURS_4Y, HOURS_5M, HOURS_5Y, HOURS_6M,
|
||||
HOURS_6Y, HOURS_7Y, HOURS_8Y,
|
||||
};
|
||||
|
||||
/// Max age thresholds in hours
|
||||
pub const MAX_AGE_HOURS: ByMaxAge<usize> = ByMaxAge {
|
||||
_1w: HOURS_1W,
|
||||
_1m: HOURS_1M,
|
||||
_2m: HOURS_2M,
|
||||
_3m: HOURS_3M,
|
||||
_4m: HOURS_4M,
|
||||
_5m: HOURS_5M,
|
||||
_6m: HOURS_6M,
|
||||
_1y: HOURS_1Y,
|
||||
_2y: HOURS_2Y,
|
||||
_3y: HOURS_3Y,
|
||||
_4y: HOURS_4Y,
|
||||
_5y: HOURS_5Y,
|
||||
_6y: HOURS_6Y,
|
||||
_7y: HOURS_7Y,
|
||||
_8y: HOURS_8Y,
|
||||
_10y: HOURS_10Y,
|
||||
_12y: HOURS_12Y,
|
||||
_15y: HOURS_15Y,
|
||||
};
|
||||
|
||||
/// Max age filters (LowerThan threshold in hours)
|
||||
pub const MAX_AGE_FILTERS: ByMaxAge<Filter> = ByMaxAge {
|
||||
_1w: Filter::Time(TimeFilter::LowerThan(MAX_AGE_HOURS._1w)),
|
||||
_1m: Filter::Time(TimeFilter::LowerThan(MAX_AGE_HOURS._1m)),
|
||||
_2m: Filter::Time(TimeFilter::LowerThan(MAX_AGE_HOURS._2m)),
|
||||
_3m: Filter::Time(TimeFilter::LowerThan(MAX_AGE_HOURS._3m)),
|
||||
_4m: Filter::Time(TimeFilter::LowerThan(MAX_AGE_HOURS._4m)),
|
||||
_5m: Filter::Time(TimeFilter::LowerThan(MAX_AGE_HOURS._5m)),
|
||||
_6m: Filter::Time(TimeFilter::LowerThan(MAX_AGE_HOURS._6m)),
|
||||
_1y: Filter::Time(TimeFilter::LowerThan(MAX_AGE_HOURS._1y)),
|
||||
_2y: Filter::Time(TimeFilter::LowerThan(MAX_AGE_HOURS._2y)),
|
||||
_3y: Filter::Time(TimeFilter::LowerThan(MAX_AGE_HOURS._3y)),
|
||||
_4y: Filter::Time(TimeFilter::LowerThan(MAX_AGE_HOURS._4y)),
|
||||
_5y: Filter::Time(TimeFilter::LowerThan(MAX_AGE_HOURS._5y)),
|
||||
_6y: Filter::Time(TimeFilter::LowerThan(MAX_AGE_HOURS._6y)),
|
||||
_7y: Filter::Time(TimeFilter::LowerThan(MAX_AGE_HOURS._7y)),
|
||||
_8y: Filter::Time(TimeFilter::LowerThan(MAX_AGE_HOURS._8y)),
|
||||
_10y: Filter::Time(TimeFilter::LowerThan(MAX_AGE_HOURS._10y)),
|
||||
_12y: Filter::Time(TimeFilter::LowerThan(MAX_AGE_HOURS._12y)),
|
||||
_15y: Filter::Time(TimeFilter::LowerThan(MAX_AGE_HOURS._15y)),
|
||||
};
|
||||
|
||||
/// Max age names
|
||||
pub const MAX_AGE_NAMES: ByMaxAge<CohortName> = ByMaxAge {
|
||||
_1w: CohortName::new("up_to_1w_old", "<1w", "Up to 1 Week Old"),
|
||||
_1m: CohortName::new("up_to_1m_old", "<1m", "Up to 1 Month Old"),
|
||||
_2m: CohortName::new("up_to_2m_old", "<2m", "Up to 2 Months Old"),
|
||||
_3m: CohortName::new("up_to_3m_old", "<3m", "Up to 3 Months Old"),
|
||||
_4m: CohortName::new("up_to_4m_old", "<4m", "Up to 4 Months Old"),
|
||||
_5m: CohortName::new("up_to_5m_old", "<5m", "Up to 5 Months Old"),
|
||||
_6m: CohortName::new("up_to_6m_old", "<6m", "Up to 6 Months Old"),
|
||||
_1y: CohortName::new("up_to_1y_old", "<1y", "Up to 1 Year Old"),
|
||||
_2y: CohortName::new("up_to_2y_old", "<2y", "Up to 2 Years Old"),
|
||||
_3y: CohortName::new("up_to_3y_old", "<3y", "Up to 3 Years Old"),
|
||||
_4y: CohortName::new("up_to_4y_old", "<4y", "Up to 4 Years Old"),
|
||||
_5y: CohortName::new("up_to_5y_old", "<5y", "Up to 5 Years Old"),
|
||||
_6y: CohortName::new("up_to_6y_old", "<6y", "Up to 6 Years Old"),
|
||||
_7y: CohortName::new("up_to_7y_old", "<7y", "Up to 7 Years Old"),
|
||||
_8y: CohortName::new("up_to_8y_old", "<8y", "Up to 8 Years Old"),
|
||||
_10y: CohortName::new("up_to_10y_old", "<10y", "Up to 10 Years Old"),
|
||||
_12y: CohortName::new("up_to_12y_old", "<12y", "Up to 12 Years Old"),
|
||||
_15y: CohortName::new("up_to_15y_old", "<15y", "Up to 15 Years Old"),
|
||||
};
|
||||
|
||||
#[derive(Default, Clone, Traversable, Serialize)]
|
||||
pub struct ByMaxAge<T> {
|
||||
pub _1w: T,
|
||||
pub _1m: T,
|
||||
pub _2m: T,
|
||||
pub _3m: T,
|
||||
pub _4m: T,
|
||||
pub _5m: T,
|
||||
pub _6m: T,
|
||||
pub _1y: T,
|
||||
pub _2y: T,
|
||||
pub _3y: T,
|
||||
pub _4y: T,
|
||||
pub _5y: T,
|
||||
pub _6y: T,
|
||||
pub _7y: T,
|
||||
pub _8y: T,
|
||||
pub _10y: T,
|
||||
pub _12y: T,
|
||||
pub _15y: T,
|
||||
}
|
||||
|
||||
impl ByMaxAge<CohortName> {
|
||||
pub const fn names() -> &'static Self {
|
||||
&MAX_AGE_NAMES
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ByMaxAge<T> {
|
||||
pub fn new<F>(mut create: F) -> Self
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> T,
|
||||
{
|
||||
let f = MAX_AGE_FILTERS;
|
||||
let n = MAX_AGE_NAMES;
|
||||
Self {
|
||||
_1w: create(f._1w.clone(), n._1w.id),
|
||||
_1m: create(f._1m.clone(), n._1m.id),
|
||||
_2m: create(f._2m.clone(), n._2m.id),
|
||||
_3m: create(f._3m.clone(), n._3m.id),
|
||||
_4m: create(f._4m.clone(), n._4m.id),
|
||||
_5m: create(f._5m.clone(), n._5m.id),
|
||||
_6m: create(f._6m.clone(), n._6m.id),
|
||||
_1y: create(f._1y.clone(), n._1y.id),
|
||||
_2y: create(f._2y.clone(), n._2y.id),
|
||||
_3y: create(f._3y.clone(), n._3y.id),
|
||||
_4y: create(f._4y.clone(), n._4y.id),
|
||||
_5y: create(f._5y.clone(), n._5y.id),
|
||||
_6y: create(f._6y.clone(), n._6y.id),
|
||||
_7y: create(f._7y.clone(), n._7y.id),
|
||||
_8y: create(f._8y.clone(), n._8y.id),
|
||||
_10y: create(f._10y.clone(), n._10y.id),
|
||||
_12y: create(f._12y.clone(), n._12y.id),
|
||||
_15y: create(f._15y.clone(), n._15y.id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_new<F, E>(mut create: F) -> Result<Self, E>
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> Result<T, E>,
|
||||
{
|
||||
let f = MAX_AGE_FILTERS;
|
||||
let n = MAX_AGE_NAMES;
|
||||
Ok(Self {
|
||||
_1w: create(f._1w.clone(), n._1w.id)?,
|
||||
_1m: create(f._1m.clone(), n._1m.id)?,
|
||||
_2m: create(f._2m.clone(), n._2m.id)?,
|
||||
_3m: create(f._3m.clone(), n._3m.id)?,
|
||||
_4m: create(f._4m.clone(), n._4m.id)?,
|
||||
_5m: create(f._5m.clone(), n._5m.id)?,
|
||||
_6m: create(f._6m.clone(), n._6m.id)?,
|
||||
_1y: create(f._1y.clone(), n._1y.id)?,
|
||||
_2y: create(f._2y.clone(), n._2y.id)?,
|
||||
_3y: create(f._3y.clone(), n._3y.id)?,
|
||||
_4y: create(f._4y.clone(), n._4y.id)?,
|
||||
_5y: create(f._5y.clone(), n._5y.id)?,
|
||||
_6y: create(f._6y.clone(), n._6y.id)?,
|
||||
_7y: create(f._7y.clone(), n._7y.id)?,
|
||||
_8y: create(f._8y.clone(), n._8y.id)?,
|
||||
_10y: create(f._10y.clone(), n._10y.id)?,
|
||||
_12y: create(f._12y.clone(), n._12y.id)?,
|
||||
_15y: create(f._15y.clone(), n._15y.id)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
[
|
||||
&self._1w, &self._1m, &self._2m, &self._3m, &self._4m, &self._5m, &self._6m, &self._1y,
|
||||
&self._2y, &self._3y, &self._4y, &self._5y, &self._6y, &self._7y, &self._8y,
|
||||
&self._10y, &self._12y, &self._15y,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
[
|
||||
&mut self._1w,
|
||||
&mut self._1m,
|
||||
&mut self._2m,
|
||||
&mut self._3m,
|
||||
&mut self._4m,
|
||||
&mut self._5m,
|
||||
&mut self._6m,
|
||||
&mut self._1y,
|
||||
&mut self._2y,
|
||||
&mut self._3y,
|
||||
&mut self._4y,
|
||||
&mut self._5y,
|
||||
&mut self._6y,
|
||||
&mut self._7y,
|
||||
&mut self._8y,
|
||||
&mut self._10y,
|
||||
&mut self._12y,
|
||||
&mut self._15y,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
[
|
||||
&mut self._1w,
|
||||
&mut self._1m,
|
||||
&mut self._2m,
|
||||
&mut self._3m,
|
||||
&mut self._4m,
|
||||
&mut self._5m,
|
||||
&mut self._6m,
|
||||
&mut self._1y,
|
||||
&mut self._2y,
|
||||
&mut self._3y,
|
||||
&mut self._4y,
|
||||
&mut self._5y,
|
||||
&mut self._6y,
|
||||
&mut self._7y,
|
||||
&mut self._8y,
|
||||
&mut self._10y,
|
||||
&mut self._12y,
|
||||
&mut self._15y,
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
use brk_traversable::Traversable;
|
||||
use rayon::prelude::*;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::{
|
||||
CohortName, Filter, TimeFilter, HOURS_10Y, HOURS_12Y, HOURS_1D, HOURS_1M, HOURS_1W, HOURS_1Y,
|
||||
HOURS_2M, HOURS_2Y, HOURS_3M, HOURS_3Y, HOURS_4M, HOURS_4Y, HOURS_5M, HOURS_5Y, HOURS_6M,
|
||||
HOURS_6Y, HOURS_7Y, HOURS_8Y,
|
||||
};
|
||||
|
||||
/// Min age thresholds in hours
|
||||
pub const MIN_AGE_HOURS: ByMinAge<usize> = ByMinAge {
|
||||
_1d: HOURS_1D,
|
||||
_1w: HOURS_1W,
|
||||
_1m: HOURS_1M,
|
||||
_2m: HOURS_2M,
|
||||
_3m: HOURS_3M,
|
||||
_4m: HOURS_4M,
|
||||
_5m: HOURS_5M,
|
||||
_6m: HOURS_6M,
|
||||
_1y: HOURS_1Y,
|
||||
_2y: HOURS_2Y,
|
||||
_3y: HOURS_3Y,
|
||||
_4y: HOURS_4Y,
|
||||
_5y: HOURS_5Y,
|
||||
_6y: HOURS_6Y,
|
||||
_7y: HOURS_7Y,
|
||||
_8y: HOURS_8Y,
|
||||
_10y: HOURS_10Y,
|
||||
_12y: HOURS_12Y,
|
||||
};
|
||||
|
||||
/// Min age filters (GreaterOrEqual threshold in hours)
|
||||
pub const MIN_AGE_FILTERS: ByMinAge<Filter> = ByMinAge {
|
||||
_1d: Filter::Time(TimeFilter::GreaterOrEqual(MIN_AGE_HOURS._1d)),
|
||||
_1w: Filter::Time(TimeFilter::GreaterOrEqual(MIN_AGE_HOURS._1w)),
|
||||
_1m: Filter::Time(TimeFilter::GreaterOrEqual(MIN_AGE_HOURS._1m)),
|
||||
_2m: Filter::Time(TimeFilter::GreaterOrEqual(MIN_AGE_HOURS._2m)),
|
||||
_3m: Filter::Time(TimeFilter::GreaterOrEqual(MIN_AGE_HOURS._3m)),
|
||||
_4m: Filter::Time(TimeFilter::GreaterOrEqual(MIN_AGE_HOURS._4m)),
|
||||
_5m: Filter::Time(TimeFilter::GreaterOrEqual(MIN_AGE_HOURS._5m)),
|
||||
_6m: Filter::Time(TimeFilter::GreaterOrEqual(MIN_AGE_HOURS._6m)),
|
||||
_1y: Filter::Time(TimeFilter::GreaterOrEqual(MIN_AGE_HOURS._1y)),
|
||||
_2y: Filter::Time(TimeFilter::GreaterOrEqual(MIN_AGE_HOURS._2y)),
|
||||
_3y: Filter::Time(TimeFilter::GreaterOrEqual(MIN_AGE_HOURS._3y)),
|
||||
_4y: Filter::Time(TimeFilter::GreaterOrEqual(MIN_AGE_HOURS._4y)),
|
||||
_5y: Filter::Time(TimeFilter::GreaterOrEqual(MIN_AGE_HOURS._5y)),
|
||||
_6y: Filter::Time(TimeFilter::GreaterOrEqual(MIN_AGE_HOURS._6y)),
|
||||
_7y: Filter::Time(TimeFilter::GreaterOrEqual(MIN_AGE_HOURS._7y)),
|
||||
_8y: Filter::Time(TimeFilter::GreaterOrEqual(MIN_AGE_HOURS._8y)),
|
||||
_10y: Filter::Time(TimeFilter::GreaterOrEqual(MIN_AGE_HOURS._10y)),
|
||||
_12y: Filter::Time(TimeFilter::GreaterOrEqual(MIN_AGE_HOURS._12y)),
|
||||
};
|
||||
|
||||
/// Min age names
|
||||
pub const MIN_AGE_NAMES: ByMinAge<CohortName> = ByMinAge {
|
||||
_1d: CohortName::new("at_least_1d_old", "1d+", "At Least 1 Day Old"),
|
||||
_1w: CohortName::new("at_least_1w_old", "1w+", "At Least 1 Week Old"),
|
||||
_1m: CohortName::new("at_least_1m_old", "1m+", "At Least 1 Month Old"),
|
||||
_2m: CohortName::new("at_least_2m_old", "2m+", "At Least 2 Months Old"),
|
||||
_3m: CohortName::new("at_least_3m_old", "3m+", "At Least 3 Months Old"),
|
||||
_4m: CohortName::new("at_least_4m_old", "4m+", "At Least 4 Months Old"),
|
||||
_5m: CohortName::new("at_least_5m_old", "5m+", "At Least 5 Months Old"),
|
||||
_6m: CohortName::new("at_least_6m_old", "6m+", "At Least 6 Months Old"),
|
||||
_1y: CohortName::new("at_least_1y_old", "1y+", "At Least 1 Year Old"),
|
||||
_2y: CohortName::new("at_least_2y_old", "2y+", "At Least 2 Years Old"),
|
||||
_3y: CohortName::new("at_least_3y_old", "3y+", "At Least 3 Years Old"),
|
||||
_4y: CohortName::new("at_least_4y_old", "4y+", "At Least 4 Years Old"),
|
||||
_5y: CohortName::new("at_least_5y_old", "5y+", "At Least 5 Years Old"),
|
||||
_6y: CohortName::new("at_least_6y_old", "6y+", "At Least 6 Years Old"),
|
||||
_7y: CohortName::new("at_least_7y_old", "7y+", "At Least 7 Years Old"),
|
||||
_8y: CohortName::new("at_least_8y_old", "8y+", "At Least 8 Years Old"),
|
||||
_10y: CohortName::new("at_least_10y_old", "10y+", "At Least 10 Years Old"),
|
||||
_12y: CohortName::new("at_least_12y_old", "12y+", "At Least 12 Years Old"),
|
||||
};
|
||||
|
||||
#[derive(Default, Clone, Traversable, Serialize)]
|
||||
pub struct ByMinAge<T> {
|
||||
pub _1d: T,
|
||||
pub _1w: T,
|
||||
pub _1m: T,
|
||||
pub _2m: T,
|
||||
pub _3m: T,
|
||||
pub _4m: T,
|
||||
pub _5m: T,
|
||||
pub _6m: T,
|
||||
pub _1y: T,
|
||||
pub _2y: T,
|
||||
pub _3y: T,
|
||||
pub _4y: T,
|
||||
pub _5y: T,
|
||||
pub _6y: T,
|
||||
pub _7y: T,
|
||||
pub _8y: T,
|
||||
pub _10y: T,
|
||||
pub _12y: T,
|
||||
}
|
||||
|
||||
impl ByMinAge<CohortName> {
|
||||
pub const fn names() -> &'static Self {
|
||||
&MIN_AGE_NAMES
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ByMinAge<T> {
|
||||
pub fn new<F>(mut create: F) -> Self
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> T,
|
||||
{
|
||||
let f = MIN_AGE_FILTERS;
|
||||
let n = MIN_AGE_NAMES;
|
||||
Self {
|
||||
_1d: create(f._1d.clone(), n._1d.id),
|
||||
_1w: create(f._1w.clone(), n._1w.id),
|
||||
_1m: create(f._1m.clone(), n._1m.id),
|
||||
_2m: create(f._2m.clone(), n._2m.id),
|
||||
_3m: create(f._3m.clone(), n._3m.id),
|
||||
_4m: create(f._4m.clone(), n._4m.id),
|
||||
_5m: create(f._5m.clone(), n._5m.id),
|
||||
_6m: create(f._6m.clone(), n._6m.id),
|
||||
_1y: create(f._1y.clone(), n._1y.id),
|
||||
_2y: create(f._2y.clone(), n._2y.id),
|
||||
_3y: create(f._3y.clone(), n._3y.id),
|
||||
_4y: create(f._4y.clone(), n._4y.id),
|
||||
_5y: create(f._5y.clone(), n._5y.id),
|
||||
_6y: create(f._6y.clone(), n._6y.id),
|
||||
_7y: create(f._7y.clone(), n._7y.id),
|
||||
_8y: create(f._8y.clone(), n._8y.id),
|
||||
_10y: create(f._10y.clone(), n._10y.id),
|
||||
_12y: create(f._12y.clone(), n._12y.id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_new<F, E>(mut create: F) -> Result<Self, E>
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> Result<T, E>,
|
||||
{
|
||||
let f = MIN_AGE_FILTERS;
|
||||
let n = MIN_AGE_NAMES;
|
||||
Ok(Self {
|
||||
_1d: create(f._1d.clone(), n._1d.id)?,
|
||||
_1w: create(f._1w.clone(), n._1w.id)?,
|
||||
_1m: create(f._1m.clone(), n._1m.id)?,
|
||||
_2m: create(f._2m.clone(), n._2m.id)?,
|
||||
_3m: create(f._3m.clone(), n._3m.id)?,
|
||||
_4m: create(f._4m.clone(), n._4m.id)?,
|
||||
_5m: create(f._5m.clone(), n._5m.id)?,
|
||||
_6m: create(f._6m.clone(), n._6m.id)?,
|
||||
_1y: create(f._1y.clone(), n._1y.id)?,
|
||||
_2y: create(f._2y.clone(), n._2y.id)?,
|
||||
_3y: create(f._3y.clone(), n._3y.id)?,
|
||||
_4y: create(f._4y.clone(), n._4y.id)?,
|
||||
_5y: create(f._5y.clone(), n._5y.id)?,
|
||||
_6y: create(f._6y.clone(), n._6y.id)?,
|
||||
_7y: create(f._7y.clone(), n._7y.id)?,
|
||||
_8y: create(f._8y.clone(), n._8y.id)?,
|
||||
_10y: create(f._10y.clone(), n._10y.id)?,
|
||||
_12y: create(f._12y.clone(), n._12y.id)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
[
|
||||
&self._1d, &self._1w, &self._1m, &self._2m, &self._3m, &self._4m, &self._5m, &self._6m,
|
||||
&self._1y, &self._2y, &self._3y, &self._4y, &self._5y, &self._6y, &self._7y, &self._8y,
|
||||
&self._10y, &self._12y,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
[
|
||||
&mut self._1d,
|
||||
&mut self._1w,
|
||||
&mut self._1m,
|
||||
&mut self._2m,
|
||||
&mut self._3m,
|
||||
&mut self._4m,
|
||||
&mut self._5m,
|
||||
&mut self._6m,
|
||||
&mut self._1y,
|
||||
&mut self._2y,
|
||||
&mut self._3y,
|
||||
&mut self._4y,
|
||||
&mut self._5y,
|
||||
&mut self._6y,
|
||||
&mut self._7y,
|
||||
&mut self._8y,
|
||||
&mut self._10y,
|
||||
&mut self._12y,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
[
|
||||
&mut self._1d,
|
||||
&mut self._1w,
|
||||
&mut self._1m,
|
||||
&mut self._2m,
|
||||
&mut self._3m,
|
||||
&mut self._4m,
|
||||
&mut self._5m,
|
||||
&mut self._6m,
|
||||
&mut self._1y,
|
||||
&mut self._2y,
|
||||
&mut self._3y,
|
||||
&mut self._4y,
|
||||
&mut self._5y,
|
||||
&mut self._6y,
|
||||
&mut self._7y,
|
||||
&mut self._8y,
|
||||
&mut self._10y,
|
||||
&mut self._12y,
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
use std::ops::{Add, AddAssign};
|
||||
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::OutputType;
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use serde::Serialize;
|
||||
|
||||
use super::{CohortName, Filter};
|
||||
|
||||
/// Spendable type values
|
||||
pub const SPENDABLE_TYPE_VALUES: BySpendableType<OutputType> = BySpendableType {
|
||||
p2pk65: OutputType::P2PK65,
|
||||
p2pk33: OutputType::P2PK33,
|
||||
p2pkh: OutputType::P2PKH,
|
||||
p2ms: OutputType::P2MS,
|
||||
p2sh: OutputType::P2SH,
|
||||
p2wpkh: OutputType::P2WPKH,
|
||||
p2wsh: OutputType::P2WSH,
|
||||
p2tr: OutputType::P2TR,
|
||||
p2a: OutputType::P2A,
|
||||
unknown: OutputType::Unknown,
|
||||
empty: OutputType::Empty,
|
||||
};
|
||||
|
||||
/// Spendable type filters
|
||||
pub const SPENDABLE_TYPE_FILTERS: BySpendableType<Filter> = BySpendableType {
|
||||
p2pk65: Filter::Type(SPENDABLE_TYPE_VALUES.p2pk65),
|
||||
p2pk33: Filter::Type(SPENDABLE_TYPE_VALUES.p2pk33),
|
||||
p2pkh: Filter::Type(SPENDABLE_TYPE_VALUES.p2pkh),
|
||||
p2ms: Filter::Type(SPENDABLE_TYPE_VALUES.p2ms),
|
||||
p2sh: Filter::Type(SPENDABLE_TYPE_VALUES.p2sh),
|
||||
p2wpkh: Filter::Type(SPENDABLE_TYPE_VALUES.p2wpkh),
|
||||
p2wsh: Filter::Type(SPENDABLE_TYPE_VALUES.p2wsh),
|
||||
p2tr: Filter::Type(SPENDABLE_TYPE_VALUES.p2tr),
|
||||
p2a: Filter::Type(SPENDABLE_TYPE_VALUES.p2a),
|
||||
unknown: Filter::Type(SPENDABLE_TYPE_VALUES.unknown),
|
||||
empty: Filter::Type(SPENDABLE_TYPE_VALUES.empty),
|
||||
};
|
||||
|
||||
/// Spendable type names
|
||||
pub const SPENDABLE_TYPE_NAMES: BySpendableType<CohortName> = BySpendableType {
|
||||
p2pk65: CohortName::new("p2pk65", "P2PK65", "Pay to Public Key (65 bytes)"),
|
||||
p2pk33: CohortName::new("p2pk33", "P2PK33", "Pay to Public Key (33 bytes)"),
|
||||
p2pkh: CohortName::new("p2pkh", "P2PKH", "Pay to Public Key Hash"),
|
||||
p2ms: CohortName::new("p2ms", "P2MS", "Pay to Multisig"),
|
||||
p2sh: CohortName::new("p2sh", "P2SH", "Pay to Script Hash"),
|
||||
p2wpkh: CohortName::new("p2wpkh", "P2WPKH", "Pay to Witness Public Key Hash"),
|
||||
p2wsh: CohortName::new("p2wsh", "P2WSH", "Pay to Witness Script Hash"),
|
||||
p2tr: CohortName::new("p2tr", "P2TR", "Pay to Taproot"),
|
||||
p2a: CohortName::new("p2a", "P2A", "Pay to Anchor"),
|
||||
unknown: CohortName::new("unknown_outputs", "Unknown", "Unknown Output Type"),
|
||||
empty: CohortName::new("empty_outputs", "Empty", "Empty Output"),
|
||||
};
|
||||
|
||||
#[derive(Default, Clone, Debug, Traversable, Serialize)]
|
||||
pub struct BySpendableType<T> {
|
||||
pub p2pk65: T,
|
||||
pub p2pk33: T,
|
||||
pub p2pkh: T,
|
||||
pub p2ms: T,
|
||||
pub p2sh: T,
|
||||
pub p2wpkh: T,
|
||||
pub p2wsh: T,
|
||||
pub p2tr: T,
|
||||
pub p2a: T,
|
||||
pub unknown: T,
|
||||
pub empty: T,
|
||||
}
|
||||
|
||||
impl BySpendableType<CohortName> {
|
||||
pub const fn names() -> &'static Self {
|
||||
&SPENDABLE_TYPE_NAMES
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> BySpendableType<T> {
|
||||
pub fn new<F>(mut create: F) -> Self
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> T,
|
||||
{
|
||||
let f = SPENDABLE_TYPE_FILTERS;
|
||||
let n = SPENDABLE_TYPE_NAMES;
|
||||
Self {
|
||||
p2pk65: create(f.p2pk65, n.p2pk65.id),
|
||||
p2pk33: create(f.p2pk33, n.p2pk33.id),
|
||||
p2pkh: create(f.p2pkh, n.p2pkh.id),
|
||||
p2ms: create(f.p2ms, n.p2ms.id),
|
||||
p2sh: create(f.p2sh, n.p2sh.id),
|
||||
p2wpkh: create(f.p2wpkh, n.p2wpkh.id),
|
||||
p2wsh: create(f.p2wsh, n.p2wsh.id),
|
||||
p2tr: create(f.p2tr, n.p2tr.id),
|
||||
p2a: create(f.p2a, n.p2a.id),
|
||||
unknown: create(f.unknown, n.unknown.id),
|
||||
empty: create(f.empty, n.empty.id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_new<F, E>(mut create: F) -> Result<Self, E>
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> Result<T, E>,
|
||||
{
|
||||
let f = SPENDABLE_TYPE_FILTERS;
|
||||
let n = SPENDABLE_TYPE_NAMES;
|
||||
Ok(Self {
|
||||
p2pk65: create(f.p2pk65, n.p2pk65.id)?,
|
||||
p2pk33: create(f.p2pk33, n.p2pk33.id)?,
|
||||
p2pkh: create(f.p2pkh, n.p2pkh.id)?,
|
||||
p2ms: create(f.p2ms, n.p2ms.id)?,
|
||||
p2sh: create(f.p2sh, n.p2sh.id)?,
|
||||
p2wpkh: create(f.p2wpkh, n.p2wpkh.id)?,
|
||||
p2wsh: create(f.p2wsh, n.p2wsh.id)?,
|
||||
p2tr: create(f.p2tr, n.p2tr.id)?,
|
||||
p2a: create(f.p2a, n.p2a.id)?,
|
||||
unknown: create(f.unknown, n.unknown.id)?,
|
||||
empty: create(f.empty, n.empty.id)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, output_type: OutputType) -> &mut T {
|
||||
match output_type {
|
||||
OutputType::P2PK65 => &mut self.p2pk65,
|
||||
OutputType::P2PK33 => &mut self.p2pk33,
|
||||
OutputType::P2PKH => &mut self.p2pkh,
|
||||
OutputType::P2MS => &mut self.p2ms,
|
||||
OutputType::P2SH => &mut self.p2sh,
|
||||
OutputType::P2WPKH => &mut self.p2wpkh,
|
||||
OutputType::P2WSH => &mut self.p2wsh,
|
||||
OutputType::P2TR => &mut self.p2tr,
|
||||
OutputType::P2A => &mut self.p2a,
|
||||
OutputType::Unknown => &mut self.unknown,
|
||||
OutputType::Empty => &mut self.empty,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
[
|
||||
&self.p2pk65,
|
||||
&self.p2pk33,
|
||||
&self.p2pkh,
|
||||
&self.p2ms,
|
||||
&self.p2sh,
|
||||
&self.p2wpkh,
|
||||
&self.p2wsh,
|
||||
&self.p2tr,
|
||||
&self.p2a,
|
||||
&self.unknown,
|
||||
&self.empty,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
[
|
||||
&mut self.p2pk65,
|
||||
&mut self.p2pk33,
|
||||
&mut self.p2pkh,
|
||||
&mut self.p2ms,
|
||||
&mut self.p2sh,
|
||||
&mut self.p2wpkh,
|
||||
&mut self.p2wsh,
|
||||
&mut self.p2tr,
|
||||
&mut self.p2a,
|
||||
&mut self.unknown,
|
||||
&mut self.empty,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
[
|
||||
&mut self.p2pk65,
|
||||
&mut self.p2pk33,
|
||||
&mut self.p2pkh,
|
||||
&mut self.p2ms,
|
||||
&mut self.p2sh,
|
||||
&mut self.p2wpkh,
|
||||
&mut self.p2wsh,
|
||||
&mut self.p2tr,
|
||||
&mut self.p2a,
|
||||
&mut self.unknown,
|
||||
&mut self.empty,
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
|
||||
pub fn iter_typed(&self) -> impl Iterator<Item = (OutputType, &T)> {
|
||||
[
|
||||
(OutputType::P2PK65, &self.p2pk65),
|
||||
(OutputType::P2PK33, &self.p2pk33),
|
||||
(OutputType::P2PKH, &self.p2pkh),
|
||||
(OutputType::P2MS, &self.p2ms),
|
||||
(OutputType::P2SH, &self.p2sh),
|
||||
(OutputType::P2WPKH, &self.p2wpkh),
|
||||
(OutputType::P2WSH, &self.p2wsh),
|
||||
(OutputType::P2TR, &self.p2tr),
|
||||
(OutputType::P2A, &self.p2a),
|
||||
(OutputType::Unknown, &self.unknown),
|
||||
(OutputType::Empty, &self.empty),
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn iter_typed_mut(&mut self) -> impl Iterator<Item = (OutputType, &mut T)> {
|
||||
[
|
||||
(OutputType::P2PK65, &mut self.p2pk65),
|
||||
(OutputType::P2PK33, &mut self.p2pk33),
|
||||
(OutputType::P2PKH, &mut self.p2pkh),
|
||||
(OutputType::P2MS, &mut self.p2ms),
|
||||
(OutputType::P2SH, &mut self.p2sh),
|
||||
(OutputType::P2WPKH, &mut self.p2wpkh),
|
||||
(OutputType::P2WSH, &mut self.p2wsh),
|
||||
(OutputType::P2TR, &mut self.p2tr),
|
||||
(OutputType::P2A, &mut self.p2a),
|
||||
(OutputType::Unknown, &mut self.unknown),
|
||||
(OutputType::Empty, &mut self.empty),
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Add for BySpendableType<T>
|
||||
where
|
||||
T: Add<Output = T>,
|
||||
{
|
||||
type Output = Self;
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self {
|
||||
p2pk65: self.p2pk65 + rhs.p2pk65,
|
||||
p2pk33: self.p2pk33 + rhs.p2pk33,
|
||||
p2pkh: self.p2pkh + rhs.p2pkh,
|
||||
p2ms: self.p2ms + rhs.p2ms,
|
||||
p2sh: self.p2sh + rhs.p2sh,
|
||||
p2wpkh: self.p2wpkh + rhs.p2wpkh,
|
||||
p2wsh: self.p2wsh + rhs.p2wsh,
|
||||
p2tr: self.p2tr + rhs.p2tr,
|
||||
p2a: self.p2a + rhs.p2a,
|
||||
unknown: self.unknown + rhs.unknown,
|
||||
empty: self.empty + rhs.empty,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AddAssign for BySpendableType<T>
|
||||
where
|
||||
T: AddAssign,
|
||||
{
|
||||
fn add_assign(&mut self, rhs: Self) {
|
||||
self.p2pk65 += rhs.p2pk65;
|
||||
self.p2pk33 += rhs.p2pk33;
|
||||
self.p2pkh += rhs.p2pkh;
|
||||
self.p2ms += rhs.p2ms;
|
||||
self.p2sh += rhs.p2sh;
|
||||
self.p2wpkh += rhs.p2wpkh;
|
||||
self.p2wsh += rhs.p2wsh;
|
||||
self.p2tr += rhs.p2tr;
|
||||
self.p2a += rhs.p2a;
|
||||
self.unknown += rhs.unknown;
|
||||
self.empty += rhs.empty;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,34 @@
|
||||
use std::ops::{Add, AddAssign};
|
||||
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::OutputType;
|
||||
use rayon::prelude::*;
|
||||
|
||||
use super::{BySpendableType, ByUnspendableType};
|
||||
use super::{Filter, SpendableType, UnspendableType};
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct GroupedByType<T> {
|
||||
pub spendable: BySpendableType<T>,
|
||||
pub unspendable: ByUnspendableType<T>,
|
||||
pub const OP_RETURN: &str = "op_return";
|
||||
|
||||
#[derive(Default, Clone, Debug, Traversable)]
|
||||
pub struct ByType<T> {
|
||||
#[traversable(flatten)]
|
||||
pub spendable: SpendableType<T>,
|
||||
#[traversable(flatten)]
|
||||
pub unspendable: UnspendableType<T>,
|
||||
}
|
||||
|
||||
impl<T> GroupedByType<T> {
|
||||
impl<T> ByType<T> {
|
||||
pub fn try_new<F, E>(mut create: F) -> Result<Self, E>
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> Result<T, E>,
|
||||
{
|
||||
Ok(Self {
|
||||
spendable: SpendableType::try_new(&mut create)?,
|
||||
unspendable: UnspendableType {
|
||||
op_return: create(Filter::Type(OutputType::OpReturn), OP_RETURN)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get(&self, output_type: OutputType) -> &T {
|
||||
match output_type {
|
||||
OutputType::P2PK65 => &self.spendable.p2pk65,
|
||||
@@ -24,7 +42,7 @@ impl<T> GroupedByType<T> {
|
||||
OutputType::P2A => &self.spendable.p2a,
|
||||
OutputType::Empty => &self.spendable.empty,
|
||||
OutputType::Unknown => &self.spendable.unknown,
|
||||
OutputType::OpReturn => &self.unspendable.opreturn,
|
||||
OutputType::OpReturn => &self.unspendable.op_return,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,12 +59,51 @@ impl<T> GroupedByType<T> {
|
||||
OutputType::P2A => &mut self.spendable.p2a,
|
||||
OutputType::Unknown => &mut self.spendable.unknown,
|
||||
OutputType::Empty => &mut self.spendable.empty,
|
||||
OutputType::OpReturn => &mut self.unspendable.opreturn,
|
||||
OutputType::OpReturn => &mut self.unspendable.op_return,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
self.spendable
|
||||
.iter()
|
||||
.chain(std::iter::once(&self.unspendable.op_return))
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
self.spendable
|
||||
.iter_mut()
|
||||
.chain(std::iter::once(&mut self.unspendable.op_return))
|
||||
}
|
||||
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
let Self {
|
||||
spendable,
|
||||
unspendable,
|
||||
} = self;
|
||||
spendable
|
||||
.par_iter_mut()
|
||||
.chain([&mut unspendable.op_return].into_par_iter())
|
||||
}
|
||||
|
||||
pub fn iter_typed(&self) -> impl Iterator<Item = (OutputType, &T)> {
|
||||
self.spendable.iter_typed().chain(std::iter::once((
|
||||
OutputType::OpReturn,
|
||||
&self.unspendable.op_return,
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn iter_typed_mut(&mut self) -> impl Iterator<Item = (OutputType, &mut T)> {
|
||||
self.spendable.iter_typed_mut().chain(std::iter::once((
|
||||
OutputType::OpReturn,
|
||||
&mut self.unspendable.op_return,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Add for GroupedByType<T>
|
||||
impl<T> Add for ByType<T>
|
||||
where
|
||||
T: Add<Output = T>,
|
||||
{
|
||||
@@ -59,7 +116,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AddAssign for GroupedByType<T>
|
||||
impl<T> AddAssign for ByType<T>
|
||||
where
|
||||
T: AddAssign,
|
||||
{
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
use std::ops::{Add, AddAssign};
|
||||
|
||||
use brk_traversable::Traversable;
|
||||
|
||||
#[derive(Default, Clone, Debug, Traversable)]
|
||||
pub struct ByUnspendableType<T> {
|
||||
pub opreturn: T,
|
||||
}
|
||||
|
||||
impl<T> ByUnspendableType<T> {
|
||||
pub fn as_vec(&self) -> [&T; 1] {
|
||||
[&self.opreturn]
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Add for ByUnspendableType<T>
|
||||
where
|
||||
T: Add<Output = T>,
|
||||
{
|
||||
type Output = Self;
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self {
|
||||
opreturn: self.opreturn + rhs.opreturn,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AddAssign for ByUnspendableType<T>
|
||||
where
|
||||
T: AddAssign,
|
||||
{
|
||||
fn add_assign(&mut self, rhs: Self) {
|
||||
self.opreturn += rhs.opreturn;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
#[derive(Default, Clone)]
|
||||
pub struct GroupedByValue<T> {
|
||||
pub up_to_1cent: T,
|
||||
pub from_1c_to_10c: T,
|
||||
pub from_10c_to_1d: T,
|
||||
pub from_1d_to_10d: T,
|
||||
pub from_10usd_to_100usd: T,
|
||||
pub from_100usd_to_1_000usd: T,
|
||||
pub from_1_000usd_to_10_000usd: T,
|
||||
pub from_10_000usd_to_100_000usd: T,
|
||||
pub from_100_000usd_to_1_000_000usd: T,
|
||||
pub from_1_000_000usd_to_10_000_000usd: T,
|
||||
pub from_10_000_000usd_to_100_000_000usd: T,
|
||||
pub from_100_000_000usd_to_1_000_000_000usd: T,
|
||||
pub from_1_000_000_000usd: T,
|
||||
// ...
|
||||
}
|
||||
|
||||
impl<T> GroupedByValue<T> {
|
||||
pub fn as_mut_vec(&mut self) -> Vec<&mut T> {
|
||||
vec![
|
||||
&mut self.up_to_1cent,
|
||||
&mut self.from_1c_to_10c,
|
||||
&mut self.from_10c_to_1d,
|
||||
&mut self.from_1d_to_10d,
|
||||
&mut self.from_10usd_to_100usd,
|
||||
&mut self.from_100usd_to_1_000usd,
|
||||
&mut self.from_1_000usd_to_10_000usd,
|
||||
&mut self.from_10_000usd_to_100_000usd,
|
||||
&mut self.from_100_000usd_to_1_000_000usd,
|
||||
&mut self.from_1_000_000usd_to_10_000_000usd,
|
||||
&mut self.from_10_000_000usd_to_100_000_000usd,
|
||||
&mut self.from_100_000_000usd_to_1_000_000_000usd,
|
||||
&mut self.from_1_000_000_000usd,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Timestamp, Year};
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use serde::Serialize;
|
||||
|
||||
use super::{CohortName, Filter};
|
||||
|
||||
/// Year values
|
||||
pub const YEAR_VALUES: ByYear<Year> = ByYear {
|
||||
_2009: Year::new(2009),
|
||||
_2010: Year::new(2010),
|
||||
_2011: Year::new(2011),
|
||||
_2012: Year::new(2012),
|
||||
_2013: Year::new(2013),
|
||||
_2014: Year::new(2014),
|
||||
_2015: Year::new(2015),
|
||||
_2016: Year::new(2016),
|
||||
_2017: Year::new(2017),
|
||||
_2018: Year::new(2018),
|
||||
_2019: Year::new(2019),
|
||||
_2020: Year::new(2020),
|
||||
_2021: Year::new(2021),
|
||||
_2022: Year::new(2022),
|
||||
_2023: Year::new(2023),
|
||||
_2024: Year::new(2024),
|
||||
_2025: Year::new(2025),
|
||||
_2026: Year::new(2026),
|
||||
};
|
||||
|
||||
/// Year filters
|
||||
pub const YEAR_FILTERS: ByYear<Filter> = ByYear {
|
||||
_2009: Filter::Year(YEAR_VALUES._2009),
|
||||
_2010: Filter::Year(YEAR_VALUES._2010),
|
||||
_2011: Filter::Year(YEAR_VALUES._2011),
|
||||
_2012: Filter::Year(YEAR_VALUES._2012),
|
||||
_2013: Filter::Year(YEAR_VALUES._2013),
|
||||
_2014: Filter::Year(YEAR_VALUES._2014),
|
||||
_2015: Filter::Year(YEAR_VALUES._2015),
|
||||
_2016: Filter::Year(YEAR_VALUES._2016),
|
||||
_2017: Filter::Year(YEAR_VALUES._2017),
|
||||
_2018: Filter::Year(YEAR_VALUES._2018),
|
||||
_2019: Filter::Year(YEAR_VALUES._2019),
|
||||
_2020: Filter::Year(YEAR_VALUES._2020),
|
||||
_2021: Filter::Year(YEAR_VALUES._2021),
|
||||
_2022: Filter::Year(YEAR_VALUES._2022),
|
||||
_2023: Filter::Year(YEAR_VALUES._2023),
|
||||
_2024: Filter::Year(YEAR_VALUES._2024),
|
||||
_2025: Filter::Year(YEAR_VALUES._2025),
|
||||
_2026: Filter::Year(YEAR_VALUES._2026),
|
||||
};
|
||||
|
||||
/// Year names
|
||||
pub const YEAR_NAMES: ByYear<CohortName> = ByYear {
|
||||
_2009: CohortName::new("year_2009", "2009", "Year 2009"),
|
||||
_2010: CohortName::new("year_2010", "2010", "Year 2010"),
|
||||
_2011: CohortName::new("year_2011", "2011", "Year 2011"),
|
||||
_2012: CohortName::new("year_2012", "2012", "Year 2012"),
|
||||
_2013: CohortName::new("year_2013", "2013", "Year 2013"),
|
||||
_2014: CohortName::new("year_2014", "2014", "Year 2014"),
|
||||
_2015: CohortName::new("year_2015", "2015", "Year 2015"),
|
||||
_2016: CohortName::new("year_2016", "2016", "Year 2016"),
|
||||
_2017: CohortName::new("year_2017", "2017", "Year 2017"),
|
||||
_2018: CohortName::new("year_2018", "2018", "Year 2018"),
|
||||
_2019: CohortName::new("year_2019", "2019", "Year 2019"),
|
||||
_2020: CohortName::new("year_2020", "2020", "Year 2020"),
|
||||
_2021: CohortName::new("year_2021", "2021", "Year 2021"),
|
||||
_2022: CohortName::new("year_2022", "2022", "Year 2022"),
|
||||
_2023: CohortName::new("year_2023", "2023", "Year 2023"),
|
||||
_2024: CohortName::new("year_2024", "2024", "Year 2024"),
|
||||
_2025: CohortName::new("year_2025", "2025", "Year 2025"),
|
||||
_2026: CohortName::new("year_2026", "2026", "Year 2026"),
|
||||
};
|
||||
|
||||
#[derive(Default, Clone, Traversable, Serialize)]
|
||||
pub struct ByYear<T> {
|
||||
pub _2009: T,
|
||||
pub _2010: T,
|
||||
pub _2011: T,
|
||||
pub _2012: T,
|
||||
pub _2013: T,
|
||||
pub _2014: T,
|
||||
pub _2015: T,
|
||||
pub _2016: T,
|
||||
pub _2017: T,
|
||||
pub _2018: T,
|
||||
pub _2019: T,
|
||||
pub _2020: T,
|
||||
pub _2021: T,
|
||||
pub _2022: T,
|
||||
pub _2023: T,
|
||||
pub _2024: T,
|
||||
pub _2025: T,
|
||||
pub _2026: T,
|
||||
}
|
||||
|
||||
impl ByYear<CohortName> {
|
||||
pub const fn names() -> &'static Self {
|
||||
&YEAR_NAMES
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ByYear<T> {
|
||||
pub fn new<F>(mut create: F) -> Self
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> T,
|
||||
{
|
||||
let f = YEAR_FILTERS;
|
||||
let n = YEAR_NAMES;
|
||||
Self {
|
||||
_2009: create(f._2009, n._2009.id),
|
||||
_2010: create(f._2010, n._2010.id),
|
||||
_2011: create(f._2011, n._2011.id),
|
||||
_2012: create(f._2012, n._2012.id),
|
||||
_2013: create(f._2013, n._2013.id),
|
||||
_2014: create(f._2014, n._2014.id),
|
||||
_2015: create(f._2015, n._2015.id),
|
||||
_2016: create(f._2016, n._2016.id),
|
||||
_2017: create(f._2017, n._2017.id),
|
||||
_2018: create(f._2018, n._2018.id),
|
||||
_2019: create(f._2019, n._2019.id),
|
||||
_2020: create(f._2020, n._2020.id),
|
||||
_2021: create(f._2021, n._2021.id),
|
||||
_2022: create(f._2022, n._2022.id),
|
||||
_2023: create(f._2023, n._2023.id),
|
||||
_2024: create(f._2024, n._2024.id),
|
||||
_2025: create(f._2025, n._2025.id),
|
||||
_2026: create(f._2026, n._2026.id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_new<F, E>(mut create: F) -> Result<Self, E>
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> Result<T, E>,
|
||||
{
|
||||
let f = YEAR_FILTERS;
|
||||
let n = YEAR_NAMES;
|
||||
Ok(Self {
|
||||
_2009: create(f._2009, n._2009.id)?,
|
||||
_2010: create(f._2010, n._2010.id)?,
|
||||
_2011: create(f._2011, n._2011.id)?,
|
||||
_2012: create(f._2012, n._2012.id)?,
|
||||
_2013: create(f._2013, n._2013.id)?,
|
||||
_2014: create(f._2014, n._2014.id)?,
|
||||
_2015: create(f._2015, n._2015.id)?,
|
||||
_2016: create(f._2016, n._2016.id)?,
|
||||
_2017: create(f._2017, n._2017.id)?,
|
||||
_2018: create(f._2018, n._2018.id)?,
|
||||
_2019: create(f._2019, n._2019.id)?,
|
||||
_2020: create(f._2020, n._2020.id)?,
|
||||
_2021: create(f._2021, n._2021.id)?,
|
||||
_2022: create(f._2022, n._2022.id)?,
|
||||
_2023: create(f._2023, n._2023.id)?,
|
||||
_2024: create(f._2024, n._2024.id)?,
|
||||
_2025: create(f._2025, n._2025.id)?,
|
||||
_2026: create(f._2026, n._2026.id)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
[
|
||||
&self._2009,
|
||||
&self._2010,
|
||||
&self._2011,
|
||||
&self._2012,
|
||||
&self._2013,
|
||||
&self._2014,
|
||||
&self._2015,
|
||||
&self._2016,
|
||||
&self._2017,
|
||||
&self._2018,
|
||||
&self._2019,
|
||||
&self._2020,
|
||||
&self._2021,
|
||||
&self._2022,
|
||||
&self._2023,
|
||||
&self._2024,
|
||||
&self._2025,
|
||||
&self._2026,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
[
|
||||
&mut self._2009,
|
||||
&mut self._2010,
|
||||
&mut self._2011,
|
||||
&mut self._2012,
|
||||
&mut self._2013,
|
||||
&mut self._2014,
|
||||
&mut self._2015,
|
||||
&mut self._2016,
|
||||
&mut self._2017,
|
||||
&mut self._2018,
|
||||
&mut self._2019,
|
||||
&mut self._2020,
|
||||
&mut self._2021,
|
||||
&mut self._2022,
|
||||
&mut self._2023,
|
||||
&mut self._2024,
|
||||
&mut self._2025,
|
||||
&mut self._2026,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
[
|
||||
&mut self._2009,
|
||||
&mut self._2010,
|
||||
&mut self._2011,
|
||||
&mut self._2012,
|
||||
&mut self._2013,
|
||||
&mut self._2014,
|
||||
&mut self._2015,
|
||||
&mut self._2016,
|
||||
&mut self._2017,
|
||||
&mut self._2018,
|
||||
&mut self._2019,
|
||||
&mut self._2020,
|
||||
&mut self._2021,
|
||||
&mut self._2022,
|
||||
&mut self._2023,
|
||||
&mut self._2024,
|
||||
&mut self._2025,
|
||||
&mut self._2026,
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
|
||||
pub fn mut_vec_from_timestamp(&mut self, timestamp: Timestamp) -> &mut T {
|
||||
let year = Year::from(timestamp);
|
||||
self.get_mut(year)
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, year: Year) -> &mut T {
|
||||
match u16::from(year) {
|
||||
2009 => &mut self._2009,
|
||||
2010 => &mut self._2010,
|
||||
2011 => &mut self._2011,
|
||||
2012 => &mut self._2012,
|
||||
2013 => &mut self._2013,
|
||||
2014 => &mut self._2014,
|
||||
2015 => &mut self._2015,
|
||||
2016 => &mut self._2016,
|
||||
2017 => &mut self._2017,
|
||||
2018 => &mut self._2018,
|
||||
2019 => &mut self._2019,
|
||||
2020 => &mut self._2020,
|
||||
2021 => &mut self._2021,
|
||||
2022 => &mut self._2022,
|
||||
2023 => &mut self._2023,
|
||||
2024 => &mut self._2024,
|
||||
2025 => &mut self._2025,
|
||||
2026 => &mut self._2026,
|
||||
_ => todo!("Year {} not yet supported", u16::from(year)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Timestamp, Year};
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use serde::Serialize;
|
||||
|
||||
use super::{CohortName, Filter};
|
||||
|
||||
/// Class values
|
||||
pub const CLASS_VALUES: Class<Year> = Class {
|
||||
_2009: Year::new(2009),
|
||||
_2010: Year::new(2010),
|
||||
_2011: Year::new(2011),
|
||||
_2012: Year::new(2012),
|
||||
_2013: Year::new(2013),
|
||||
_2014: Year::new(2014),
|
||||
_2015: Year::new(2015),
|
||||
_2016: Year::new(2016),
|
||||
_2017: Year::new(2017),
|
||||
_2018: Year::new(2018),
|
||||
_2019: Year::new(2019),
|
||||
_2020: Year::new(2020),
|
||||
_2021: Year::new(2021),
|
||||
_2022: Year::new(2022),
|
||||
_2023: Year::new(2023),
|
||||
_2024: Year::new(2024),
|
||||
_2025: Year::new(2025),
|
||||
_2026: Year::new(2026),
|
||||
};
|
||||
|
||||
/// Class filters
|
||||
pub const CLASS_FILTERS: Class<Filter> = Class {
|
||||
_2009: Filter::Class(CLASS_VALUES._2009),
|
||||
_2010: Filter::Class(CLASS_VALUES._2010),
|
||||
_2011: Filter::Class(CLASS_VALUES._2011),
|
||||
_2012: Filter::Class(CLASS_VALUES._2012),
|
||||
_2013: Filter::Class(CLASS_VALUES._2013),
|
||||
_2014: Filter::Class(CLASS_VALUES._2014),
|
||||
_2015: Filter::Class(CLASS_VALUES._2015),
|
||||
_2016: Filter::Class(CLASS_VALUES._2016),
|
||||
_2017: Filter::Class(CLASS_VALUES._2017),
|
||||
_2018: Filter::Class(CLASS_VALUES._2018),
|
||||
_2019: Filter::Class(CLASS_VALUES._2019),
|
||||
_2020: Filter::Class(CLASS_VALUES._2020),
|
||||
_2021: Filter::Class(CLASS_VALUES._2021),
|
||||
_2022: Filter::Class(CLASS_VALUES._2022),
|
||||
_2023: Filter::Class(CLASS_VALUES._2023),
|
||||
_2024: Filter::Class(CLASS_VALUES._2024),
|
||||
_2025: Filter::Class(CLASS_VALUES._2025),
|
||||
_2026: Filter::Class(CLASS_VALUES._2026),
|
||||
};
|
||||
|
||||
/// Class names
|
||||
pub const CLASS_NAMES: Class<CohortName> = Class {
|
||||
_2009: CohortName::new("class_2009", "2009", "Class 2009"),
|
||||
_2010: CohortName::new("class_2010", "2010", "Class 2010"),
|
||||
_2011: CohortName::new("class_2011", "2011", "Class 2011"),
|
||||
_2012: CohortName::new("class_2012", "2012", "Class 2012"),
|
||||
_2013: CohortName::new("class_2013", "2013", "Class 2013"),
|
||||
_2014: CohortName::new("class_2014", "2014", "Class 2014"),
|
||||
_2015: CohortName::new("class_2015", "2015", "Class 2015"),
|
||||
_2016: CohortName::new("class_2016", "2016", "Class 2016"),
|
||||
_2017: CohortName::new("class_2017", "2017", "Class 2017"),
|
||||
_2018: CohortName::new("class_2018", "2018", "Class 2018"),
|
||||
_2019: CohortName::new("class_2019", "2019", "Class 2019"),
|
||||
_2020: CohortName::new("class_2020", "2020", "Class 2020"),
|
||||
_2021: CohortName::new("class_2021", "2021", "Class 2021"),
|
||||
_2022: CohortName::new("class_2022", "2022", "Class 2022"),
|
||||
_2023: CohortName::new("class_2023", "2023", "Class 2023"),
|
||||
_2024: CohortName::new("class_2024", "2024", "Class 2024"),
|
||||
_2025: CohortName::new("class_2025", "2025", "Class 2025"),
|
||||
_2026: CohortName::new("class_2026", "2026", "Class 2026"),
|
||||
};
|
||||
|
||||
#[derive(Default, Clone, Traversable, Serialize)]
|
||||
pub struct Class<T> {
|
||||
pub _2009: T,
|
||||
pub _2010: T,
|
||||
pub _2011: T,
|
||||
pub _2012: T,
|
||||
pub _2013: T,
|
||||
pub _2014: T,
|
||||
pub _2015: T,
|
||||
pub _2016: T,
|
||||
pub _2017: T,
|
||||
pub _2018: T,
|
||||
pub _2019: T,
|
||||
pub _2020: T,
|
||||
pub _2021: T,
|
||||
pub _2022: T,
|
||||
pub _2023: T,
|
||||
pub _2024: T,
|
||||
pub _2025: T,
|
||||
pub _2026: T,
|
||||
}
|
||||
|
||||
impl Class<CohortName> {
|
||||
pub const fn names() -> &'static Self {
|
||||
&CLASS_NAMES
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Class<T> {
|
||||
pub fn new<F>(mut create: F) -> Self
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> T,
|
||||
{
|
||||
let f = CLASS_FILTERS;
|
||||
let n = CLASS_NAMES;
|
||||
Self {
|
||||
_2009: create(f._2009, n._2009.id),
|
||||
_2010: create(f._2010, n._2010.id),
|
||||
_2011: create(f._2011, n._2011.id),
|
||||
_2012: create(f._2012, n._2012.id),
|
||||
_2013: create(f._2013, n._2013.id),
|
||||
_2014: create(f._2014, n._2014.id),
|
||||
_2015: create(f._2015, n._2015.id),
|
||||
_2016: create(f._2016, n._2016.id),
|
||||
_2017: create(f._2017, n._2017.id),
|
||||
_2018: create(f._2018, n._2018.id),
|
||||
_2019: create(f._2019, n._2019.id),
|
||||
_2020: create(f._2020, n._2020.id),
|
||||
_2021: create(f._2021, n._2021.id),
|
||||
_2022: create(f._2022, n._2022.id),
|
||||
_2023: create(f._2023, n._2023.id),
|
||||
_2024: create(f._2024, n._2024.id),
|
||||
_2025: create(f._2025, n._2025.id),
|
||||
_2026: create(f._2026, n._2026.id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_new<F, E>(mut create: F) -> Result<Self, E>
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> Result<T, E>,
|
||||
{
|
||||
let f = CLASS_FILTERS;
|
||||
let n = CLASS_NAMES;
|
||||
Ok(Self {
|
||||
_2009: create(f._2009, n._2009.id)?,
|
||||
_2010: create(f._2010, n._2010.id)?,
|
||||
_2011: create(f._2011, n._2011.id)?,
|
||||
_2012: create(f._2012, n._2012.id)?,
|
||||
_2013: create(f._2013, n._2013.id)?,
|
||||
_2014: create(f._2014, n._2014.id)?,
|
||||
_2015: create(f._2015, n._2015.id)?,
|
||||
_2016: create(f._2016, n._2016.id)?,
|
||||
_2017: create(f._2017, n._2017.id)?,
|
||||
_2018: create(f._2018, n._2018.id)?,
|
||||
_2019: create(f._2019, n._2019.id)?,
|
||||
_2020: create(f._2020, n._2020.id)?,
|
||||
_2021: create(f._2021, n._2021.id)?,
|
||||
_2022: create(f._2022, n._2022.id)?,
|
||||
_2023: create(f._2023, n._2023.id)?,
|
||||
_2024: create(f._2024, n._2024.id)?,
|
||||
_2025: create(f._2025, n._2025.id)?,
|
||||
_2026: create(f._2026, n._2026.id)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
[
|
||||
&self._2009,
|
||||
&self._2010,
|
||||
&self._2011,
|
||||
&self._2012,
|
||||
&self._2013,
|
||||
&self._2014,
|
||||
&self._2015,
|
||||
&self._2016,
|
||||
&self._2017,
|
||||
&self._2018,
|
||||
&self._2019,
|
||||
&self._2020,
|
||||
&self._2021,
|
||||
&self._2022,
|
||||
&self._2023,
|
||||
&self._2024,
|
||||
&self._2025,
|
||||
&self._2026,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
[
|
||||
&mut self._2009,
|
||||
&mut self._2010,
|
||||
&mut self._2011,
|
||||
&mut self._2012,
|
||||
&mut self._2013,
|
||||
&mut self._2014,
|
||||
&mut self._2015,
|
||||
&mut self._2016,
|
||||
&mut self._2017,
|
||||
&mut self._2018,
|
||||
&mut self._2019,
|
||||
&mut self._2020,
|
||||
&mut self._2021,
|
||||
&mut self._2022,
|
||||
&mut self._2023,
|
||||
&mut self._2024,
|
||||
&mut self._2025,
|
||||
&mut self._2026,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
[
|
||||
&mut self._2009,
|
||||
&mut self._2010,
|
||||
&mut self._2011,
|
||||
&mut self._2012,
|
||||
&mut self._2013,
|
||||
&mut self._2014,
|
||||
&mut self._2015,
|
||||
&mut self._2016,
|
||||
&mut self._2017,
|
||||
&mut self._2018,
|
||||
&mut self._2019,
|
||||
&mut self._2020,
|
||||
&mut self._2021,
|
||||
&mut self._2022,
|
||||
&mut self._2023,
|
||||
&mut self._2024,
|
||||
&mut self._2025,
|
||||
&mut self._2026,
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
|
||||
pub fn mut_vec_from_timestamp(&mut self, timestamp: Timestamp) -> Option<&mut T> {
|
||||
let year = Year::from(timestamp);
|
||||
self.get_mut(year)
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, year: Year) -> Option<&mut T> {
|
||||
match u16::from(year) {
|
||||
2009 => Some(&mut self._2009),
|
||||
2010 => Some(&mut self._2010),
|
||||
2011 => Some(&mut self._2011),
|
||||
2012 => Some(&mut self._2012),
|
||||
2013 => Some(&mut self._2013),
|
||||
2014 => Some(&mut self._2014),
|
||||
2015 => Some(&mut self._2015),
|
||||
2016 => Some(&mut self._2016),
|
||||
2017 => Some(&mut self._2017),
|
||||
2018 => Some(&mut self._2018),
|
||||
2019 => Some(&mut self._2019),
|
||||
2020 => Some(&mut self._2020),
|
||||
2021 => Some(&mut self._2021),
|
||||
2022 => Some(&mut self._2022),
|
||||
2023 => Some(&mut self._2023),
|
||||
2024 => Some(&mut self._2024),
|
||||
2025 => Some(&mut self._2025),
|
||||
2026 => Some(&mut self._2026),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,14 @@ pub enum CohortContext {
|
||||
/// UTXO-based cohorts: uses "utxos_" prefix for Time/Amount filters
|
||||
Utxo,
|
||||
/// Address-based cohorts: uses "addrs_" prefix for Amount filters
|
||||
Address,
|
||||
Addr,
|
||||
}
|
||||
|
||||
impl CohortContext {
|
||||
pub fn prefix(&self) -> &'static str {
|
||||
match self {
|
||||
CohortContext::Utxo => "utxos",
|
||||
CohortContext::Address => "addrs",
|
||||
CohortContext::Addr => "addrs",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,13 +24,15 @@ impl CohortContext {
|
||||
/// Build full name for a filter, adding prefix only for Time/Amount filters.
|
||||
///
|
||||
/// Prefix rules:
|
||||
/// - No prefix: `All`, `Term`, `Epoch`, `Year`, `Type`
|
||||
/// - No prefix: `All`, `Term`, `Epoch`, `Class`, `Type`
|
||||
/// - Context prefix: `Time`, `Amount`
|
||||
pub fn full_name(&self, filter: &Filter, name: &str) -> String {
|
||||
match filter {
|
||||
Filter::All | Filter::Term(_) | Filter::Epoch(_) | Filter::Year(_) | Filter::Type(_) => {
|
||||
name.to_string()
|
||||
}
|
||||
Filter::All
|
||||
| Filter::Term(_)
|
||||
| Filter::Epoch(_)
|
||||
| Filter::Class(_)
|
||||
| Filter::Type(_) => name.to_string(),
|
||||
Filter::Time(_) | Filter::Amount(_) => self.prefixed(name),
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user