Compare commits

...

866 Commits

Author SHA1 Message Date
nym21 7a2ba17d20 release: v0.1.0-alpha.4 2026-01-17 22:48:29 +01:00
nym21 ac30f0e512 docs: update generated docs 2026-01-17 22:48:00 +01:00
nym21 2e1037ff36 global: snapshot 2026-01-17 22:35:13 +01:00
nym21 626c52044d js: readme 2026-01-17 12:49:38 +01:00
nym21 f7ee4e487a server: snapshot 2026-01-17 11:23:04 +01:00
nym21 7b3e172948 global: snapshot 2026-01-17 02:34:08 +01:00
nym21 6bb1a2a311 global: snapshot 2026-01-16 23:49:49 +01:00
nym21 3b00a92fa4 global: snapshot 2026-01-16 15:17:42 +01:00
nym21 f39681bb2b price: snapshot 2026-01-16 00:41:25 +01:00
nym21 967d2c7f35 global: snapshot 2026-01-15 23:34:43 +01:00
nym21 b0d933a7ab publish: snapshot 2026-01-14 23:14:28 +01:00
nym21 96e0df110e server: add symlink to website 2026-01-14 23:00:31 +01:00
nym21 91a6129e8d release: v0.1.0-alpha.3 2026-01-14 22:47:25 +01:00
nym21 d9c829c3c6 docs: update generated docs 2026-01-14 22:46:51 +01:00
nym21 467dfcc4b8 global: snapshot 2026-01-14 22:20:23 +01:00
nym21 8a938c00f6 readme: updated 2026-01-14 20:40:42 +01:00
nym21 5661735f3e readme: updated 2026-01-14 20:35:07 +01:00
nym21 1c7434ff83 global: snapshot 2026-01-14 20:09:51 +01:00
nym21 d75c2a881b global: snapshot 2026-01-14 16:38:53 +01:00
nym21 ddb1db7a8e clients: snapshot 2026-01-14 11:12:31 +01:00
nym21 407a365055 clients: snapshot 2026-01-14 10:33:58 +01:00
nym21 335cbce09e clients: snapshot 2026-01-14 10:07:27 +01:00
nym21 922a0abb60 clients: snapshot 2026-01-14 09:37:43 +01:00
nym21 25a0ebe51e clients: snapshot 2026-01-14 01:20:25 +01:00
nym21 3a836ab0f4 clients: snapshot 2026-01-14 00:39:28 +01:00
nym21 524ab3de05 clients: snapshot 2026-01-13 23:14:26 +01:00
nym21 e77993fb76 global: snapshot 2026-01-13 22:32:29 +01:00
nym21 0c442b4a71 computer: shorten percentiles path in tree 2026-01-13 01:49:13 +01:00
nym21 670aa95494 global: snapshot 2026-01-13 01:18:27 +01:00
nym21 5ffb66c0dc global: snapshot 2026-01-12 22:43:56 +01:00
nym21 b675b70067 global: snapshot 2026-01-12 16:19:23 +01:00
nym21 1484eae53c server: endpoint description 2026-01-12 12:38:34 +01:00
nym21 b12a72ea1a server: snapshot 2026-01-12 12:34:30 +01:00
nym21 1b9e18f98b global: snapshot 2026-01-12 11:39:44 +01:00
nym21 8fe0af349d clients: snapshot 2026-01-12 08:48:12 +01:00
nym21 5826d78e35 clients: snapshot 2026-01-11 23:08:08 +01:00
nym21 325811fee7 clients: snapshot 2026-01-11 19:15:29 +01:00
nym21 69f6d32d4a global: snapshot 2026-01-11 18:55:40 +01:00
nym21 ea70c381de global: snapshot 2026-01-11 17:19:00 +01:00
nym21 6f45ec13f3 global: snapshot 2026-01-10 18:43:18 +01:00
nym21 3bc0615000 computer: renames 2026-01-10 10:23:29 +01:00
nym21 69729842a4 computer: renames 2026-01-09 23:40:00 +01:00
nym21 5f4fc646f5 computer: renames 2026-01-09 23:27:09 +01:00
nym21 85570c73cb deps: upgrade 2026-01-09 22:04:37 +01:00
nym21 3a3f6b8593 computer: snapshot 2026-01-09 22:02:34 +01:00
nym21 426d7797a3 global: big snapshot 2026-01-09 20:00:20 +01:00
nym21 cb0abc324e global: MASSIVE snapshot 2026-01-07 01:16:37 +01:00
nym21 e832ffbe23 bindgen: snapshot 2026-01-06 13:48:29 +01:00
nym21 abffdec497 crates: snapshot 2026-01-04 13:20:30 +01:00
nym21 70e7e24b4f release: v0.1.0-alpha.2 2026-01-04 11:54:27 +01:00
nym21 13ab7d39d7 global: snapshot 2026-01-04 11:51:22 +01:00
nym21 3cae817915 global: BIG snapshot 2026-01-04 01:47:03 +01:00
nym21 c33444a92e global: snapshot 2026-01-02 19:23:20 +01:00
nym21 3e9b1cc2b2 global: MASSIVE snapshot 2026-01-02 19:08:20 +01:00
nym21 ac6175688d crates: snapshot 2025-12-31 00:02:50 +01:00
nym21 a6f8108165 crates: snapshot 2025-12-30 22:49:47 +01:00
nym21 8cff55a405 crates: snapshot 2025-12-30 18:09:08 +01:00
nym21 bd376f86ea crates: snapshot 2025-12-30 11:48:09 +01:00
nym21 d9f28e85af crates: snapshot 2025-12-30 11:27:39 +01:00
nym21 ed18fd55e1 crates: snapshot 2025-12-30 00:49:34 +01:00
nym21 5b06098368 binder: snapshot 2025-12-29 20:01:43 +01:00
nym21 e89a67b9a7 global: snapshot 2025-12-29 17:02:17 +01:00
nym21 445959f5b9 global: snapshot 2025-12-29 13:20:52 +01:00
nym21 647f177f31 binder: commit generated clients 2025-12-29 09:37:57 +01:00
nym21 705dbdbd7e modules: update deps 2025-12-29 09:32:51 +01:00
nym21 31d2f8ef37 computer: snapshot 2025-12-29 00:14:54 +01:00
nym21 236b4097c5 computer: snapshot 2025-12-28 20:24:38 +01:00
nym21 f5790d5c8a computer: snapshot 2025-12-28 16:35:17 +01:00
nym21 f08ac7f916 computer: snapshot 2025-12-28 14:57:25 +01:00
nym21 e77d338357 computer: snapshot 2025-12-28 10:25:55 +01:00
nym21 5d6325ae30 computer: snapshot 2025-12-28 03:19:34 +01:00
nym21 9ba77dac0f global: snapshot 2025-12-27 20:34:13 +01:00
nym21 f9856cf0aa computer: fixes 2025-12-27 18:16:30 +01:00
nym21 de93f08e93 global: snapshot 2025-12-26 22:41:36 +01:00
nym21 d538280f4b modules: cleanup 2025-12-25 22:41:48 +01:00
nym21 bbb74b76c8 global: snapshot 2025-12-25 22:21:12 +01:00
nym21 eadf93b804 deps: upgrade 2025-12-24 15:13:43 +01:00
nym21 f29443fc15 server: openapi fixes 2025-12-23 20:23:40 +01:00
nym21 75a023bdd8 server: openapi fixes 2025-12-23 19:04:19 +01:00
nym21 d30344ee3c cleanup 2025-12-22 16:22:09 +01:00
nym21 02d635d48b cleanup 2025-12-21 23:55:45 +01:00
nym21 40ec356cc3 server: fix README 2025-12-21 23:28:03 +01:00
nym21 5a5d4da57d client: add dummy main 2025-12-21 23:23:18 +01:00
nym21 efb247d104 vecdb: bump 2025-12-21 23:20:48 +01:00
nym21 457b0e24c5 global: snapshot 2025-12-21 23:12:18 +01:00
nym21 6e0ac138d8 global: improve par writes 2025-12-21 16:22:25 +01:00
nym21 26c6c92bb8 dist: enable for brk_cli 2025-12-21 14:02:27 +01:00
nym21 e1ad45f44b scripts: update: also update rust-toolchain 2025-12-21 13:59:06 +01:00
nym21 aebca14d78 toolchain: set 2025-12-21 13:44:47 +01:00
nym21 42b0d7a174 scripts: improve publish 2025-12-21 13:37:58 +01:00
nym21 a37c2474fe bencher: publish = true 2025-12-21 13:29:59 +01:00
nym21 5f308e9da7 scripts: publish 2025-12-21 13:24:47 +01:00
nym21 3aadced85d release: v0.1.0-alpha.1 2025-12-21 13:08:40 +01:00
nym21 9375d5aded readmes: add perf section 2025-12-21 13:05:22 +01:00
nym21 2c8205146c benches: ignored 2025-12-21 12:47:28 +01:00
nym21 8d5a2b911d benches: added 2025-12-21 12:23:47 +01:00
nym21 7d5de7bf24 binder: snapshot 2025-12-21 01:23:05 +01:00
nym21 4b1410855a binder: snapshot 2025-12-21 01:04:13 +01:00
nym21 78a4d1af65 binder: snapshot 2025-12-21 00:42:54 +01:00
nym21 5e3519aad4 binder: snapshot 2025-12-21 00:33:56 +01:00
nym21 4386ef47fe binder: snapshot 2025-12-20 23:52:12 +01:00
nym21 135a18d56f binder: snapshot 2025-12-20 23:24:24 +01:00
nym21 71f45479b9 binder: snapshot 2025-12-20 21:08:17 +01:00
nym21 bcb8d5bed6 binder: snapshot 2025-12-20 19:33:04 +01:00
nym21 8f19bf7350 binder: snapshot 2025-12-20 18:19:48 +01:00
nym21 25860636f0 cargo: fix path to vecdb 2025-12-20 17:02:59 +01:00
nym21 8c2402cacb global: snapshot 2025-12-20 17:02:00 +01:00
nym21 4b910ceaa7 global: snapshot 2025-12-20 11:48:37 +01:00
nym21 4a0ce6337f global: snapshot 2025-12-20 10:16:06 +01:00
nym21 e134ed11a9 global: snapshot 2025-12-19 15:48:32 +01:00
nym21 03b83846ef global: snapshot 2025-12-19 15:25:48 +01:00
nym21 7c86c803fa changelog: update 2025-12-19 00:26:44 +01:00
nym21 a31d9dc15e release: v0.1.0-alpha.0 2025-12-18 23:06:22 +01:00
nym21 57749da919 global: readmes 2025-12-18 23:05:43 +01:00
nym21 9ad3acbdf9 global: upgrade deps 2025-12-18 22:44:27 +01:00
nym21 6fa53aca9f computer: stateful snapshot 2025-12-18 22:18:28 +01:00
nym21 bd53168c4e benches: regenerated 2025-12-18 20:50:00 +01:00
nym21 08d17b4a09 computer: snapshot 2025-12-18 18:13:48 +01:00
nym21 c5657b9c31 readmes: simplified 2025-12-18 17:10:23 +01:00
nym21 549e2da05b computer: snapshot 2025-12-18 16:08:32 +01:00
nym21 c5e912593a computer: stateful snapshot 2025-12-18 15:32:47 +01:00
nym21 a86085c2db computer: stateful snapshot 2025-12-18 13:01:31 +01:00
nym21 edbec6fd5c computer: stateful snapshot 2025-12-18 11:37:33 +01:00
nym21 a76139c0ea computer: stateful snapshot 2025-12-18 11:18:18 +01:00
nym21 59f1296d56 computer: stateful snapshot 2025-12-18 10:53:47 +01:00
nym21 14ae41c7ba computer: stateful snapshot 2025-12-18 09:35:26 +01:00
nym21 df09b3aa28 computer: stateful snapshot 2025-12-17 17:08:54 +01:00
nym21 f9fad2d775 computer: stateful snapshot 2025-12-17 15:45:15 +01:00
nym21 fa609c73ba computer: stateful snapshot 2025-12-17 15:04:02 +01:00
nym21 9b2f334130 computer: stateful snapshot 2025-12-17 14:22:31 +01:00
nym21 a006cefd71 computer: new stateful 2 2025-12-16 23:39:35 +01:00
nym21 4b2ada14a0 computer: trying the new stateful 2025-12-16 21:59:13 +01:00
nym21 1ad8d8a631 global: improve errors 2025-12-16 20:49:19 +01:00
nym21 3ca83a2289 mempool: fix recommended fees 2025-12-16 20:29:08 +01:00
nym21 2ccf0ef856 server: openapi fixes 2025-12-16 20:23:01 +01:00
nym21 f7f065c6e0 server: openapi fixes 2025-12-16 18:03:23 +01:00
nym21 593af69230 server: openapi fixes 2025-12-16 16:41:25 +01:00
nym21 032f3cb66b global: snapshot 2025-12-16 00:22:30 +01:00
nym21 692a1889ab server: snapshot 2025-12-15 17:33:49 +01:00
nym21 825a4a77c0 server: snapshot 2025-12-15 16:32:45 +01:00
nym21 882a3525af query + server: more endpoints/methods/helpers 2025-12-14 21:12:10 +01:00
nym21 b491b1f41f mempool: snapshot 5 + query: new tools + server: endpoints 2025-12-14 02:06:14 +01:00
nym21 db5d784ff7 mempool: snapshot 4 2025-12-13 18:03:46 +01:00
nym21 db57db4bd9 mempool: snapshot 3 2025-12-13 17:34:34 +01:00
nym21 c5e9b75261 mempool: snapshot partial 2025-12-13 16:42:54 +01:00
nym21 c59ac62e45 mempool: snapshot 2025-12-13 16:26:29 +01:00
nym21 9c8b9b1a3b mempool: snapshot 2025-12-13 11:10:11 +01:00
nym21 158b0254ed global: snapshot 2025-12-13 10:52:00 +01:00
nym21 3526a177fc global: snapshot 2025-12-12 16:55:55 +01:00
nym21 e755f2856a benches: update 2025-12-12 01:04:51 +01:00
nym21 2ec3ca8308 computer: stateful: refactor part 2 2025-12-11 18:34:23 +01:00
nym21 1cf75b48b5 computer: stateful: refactor part 1 2025-12-11 11:26:11 +01:00
nym21 abde9ed162 global: fully replace fjall2 by fjall3 2025-12-10 17:36:12 +01:00
nym21 998db1beed global: snapshot 2025-12-10 13:22:35 +01:00
nym21 79e352d06e store: back to vec based cache 2025-12-09 18:41:25 +01:00
nym21 b8f77433b9 store: better caching layer 2025-12-09 16:37:03 +01:00
nym21 96b967f6fb indexer: massive perf boost 2025-12-09 12:32:08 +01:00
nym21 68c71e62d6 global: snapshot 2025-12-08 22:20:18 +01:00
nym21 60a38b4108 global: snapshot 2025-12-08 17:05:38 +01:00
nym21 f4a1384dc4 indexer + store: fjall v3 test (with caching layer) 2025-12-07 21:53:19 +01:00
nym21 b88f0bab56 global: snapshot 2025-12-07 19:13:41 +01:00
nym21 f23907768f global: fixes 2025-12-06 21:35:19 +01:00
nym21 f280b03cab indexer: split 2025-12-06 16:32:57 +01:00
nym21 554c0e565d computer: stateful: split common into multiple impl files 2025-12-05 19:36:40 +01:00
nym21 cfc5f7633b computer: fix flushes 2025-12-05 17:54:01 +01:00
nym21 82050c7c01 indexer: constants 2025-12-04 23:13:37 +01:00
nym21 f4edb695de indexer: fix bug 2025-12-04 23:11:21 +01:00
nym21 dc2fa233ab indexer: fix bug 2025-12-04 21:30:08 +01:00
nym21 a1f31a14be computer: snapshot 2025-12-04 00:39:22 +01:00
nym21 d27cc02e8c computer: big refactor 2025-12-03 19:33:08 +01:00
nym21 fcc74ba212 computer: fix stateful 2025-12-03 15:43:50 +01:00
nym21 f48ad577d3 computer: make aggr p2a less brittle 2025-12-03 00:30:02 +01:00
nym21 60c73f5635 computer: fix LTH p2a increment 2025-12-03 00:00:50 +01:00
nym21 24248215e9 computer: percentiles snapshot 2025-12-02 19:40:47 +01:00
nym21 b6ec133368 computer: snapshot 2025-12-01 23:23:35 +01:00
nym21 35e567cfb6 global: fixes 2025-11-29 23:33:48 +01:00
nym21 25c697cca1 global: snapshot 2025-11-29 12:15:12 +01:00
nym21 30dc695741 global: fixes 2025-11-26 22:46:58 +01:00
nym21 9e41d51702 global: snapshot 2025-11-25 18:37:14 +01:00
nym21 dc86514329 global: snapshot 2025-11-25 17:21:07 +01:00
nym21 c644781d18 global: snapshot 2025-11-25 15:39:40 +01:00
nym21 eedc0dd075 snapshot 2025-11-21 16:16:36 +01:00
nym21 c8c62b504b snapshot 2025-11-20 20:35:54 +01:00
nym21 8467e218ae snapshot 2025-11-18 21:00:59 +01:00
nym21 e8f77ab2e5 snapshot 2025-11-14 12:09:58 +01:00
nym21 1d2c927d94 global: snapshot 2025-11-11 19:21:43 +01:00
nym21 81da73bc53 global: snapshot 2025-11-11 17:41:12 +01:00
nym21 2dcbd8df99 global: snapshot 2025-11-11 09:36:24 +01:00
nym21 37f5f50867 global: snapshot 2025-11-10 13:18:41 +01:00
nym21 f6a2a0540b global: snapshot 2025-11-09 22:57:06 +01:00
nym21 dc2e847f58 global: snapshot 2025-11-09 11:25:13 +01:00
nym21 e77fe0253e global: snapshot 2025-11-08 14:43:23 +01:00
nym21 3d3787a8d9 indexer: snapshot 2025-11-07 15:13:01 +01:00
nym21 11b323ef00 global: snapshot 2025-11-06 15:17:24 +01:00
nym21 df577ca7f5 global: snapshot 2025-11-06 13:07:07 +01:00
nym21 a2ba4d89f3 global: snapshot 2025-11-05 11:14:31 +01:00
nym21 2ad55bf558 global: snapshot 2025-11-04 11:43:04 +01:00
nym21 cf08e470ef global: snapshot 2025-10-31 21:37:02 +01:00
nym21 82e59d409e global: snapshot 2025-10-26 22:30:41 +01:00
nym21 7d01e9e91e global: snapshot 2025-10-25 16:30:14 +02:00
nym21 1e4acfe124 global: snapshot 2025-10-24 12:04:10 +02:00
nym21 4f1653b086 global: snapshot 2025-10-23 18:30:29 +02:00
nym21 6cd60a064b global: snapshot 2025-10-22 12:36:35 +02:00
nym21 8072c4670c iterator: init + global: snapshot 2025-10-21 18:59:30 +02:00
nym21 4ffa2e3993 rpc: init wrapper crate + global: snapshot 2025-10-20 23:06:25 +02:00
nym21 9b230d23dd indexer: move txoutindex->txindex and txindex->height from computer 2025-10-20 13:05:46 +02:00
nym21 baa7c9cc22 store: fix hang ? 2025-10-20 12:18:48 +02:00
nym21 33a92cfad4 store: faster everything 2025-10-20 11:33:48 +02:00
nym21 e9f6295014 indexer: speed 2025-10-19 21:18:15 +02:00
nym21 71078b5bdd indexer: perf + support fjall v3 2025-10-18 18:27:59 +02:00
nym21 6cce92af22 indexer: moved to addri->txindex and addri->outpoint indexing from addri->txoutindex 2025-10-17 01:02:26 +02:00
nym21 d3b8520c41 global: rename outputindex and inputindex to txoutindex and txinindex 2025-10-14 20:39:17 +02:00
nym21 5425085953 global: snapshot + monitor: add addresses to mempool 2025-10-14 17:36:16 +02:00
nym21 db0298ac1b global: snapshot 2025-10-13 13:52:33 +02:00
nym21 7bfca87caf mempool: init 2025-10-12 17:55:21 +02:00
nym21 5f87594ead global: snapshot 2025-10-11 18:17:36 +02:00
nym21 bb46481d7f interface: create super fast searcher 2025-10-10 22:12:31 +02:00
nym21 1821d5d57b server: api + doc 2025-10-09 17:24:44 +02:00
nym21 6ad15221de server: api doc part 5 2025-10-08 20:32:27 +02:00
nym21 83d74da556 server: api doc part 4 2025-10-08 18:42:36 +02:00
nym21 114228e8eb server: api doc part 3 2025-10-08 17:48:15 +02:00
nym21 a53f89c849 server: api doc part 2 2025-10-07 22:10:32 +02:00
nym21 7ff79c3164 server: documentation part 1 2025-10-06 22:53:50 +02:00
nym21 db344749b6 server: catalog v1 2025-10-06 12:52:18 +02:00
nym21 1c6ece48a8 global: traversable 2025-10-05 23:40:04 +02:00
nym21 b622285999 global: ivecs 2025-10-05 16:05:17 +02:00
nym21 5fde0101bf vecs: add trait + derive crates 2025-10-04 23:38:54 +02:00
nym21 a6062d4c39 docs: update README and CHANGELOG 2025-10-03 14:27:37 +02:00
nym21 66f1e92cb6 release: v0.0.111 2025-10-03 14:16:00 +02:00
nym21 d9c4653f82 global: fixes 2025-10-03 14:15:23 +02:00
nym21 cfdf8fdbca changelog: update 2025-10-02 18:09:39 +02:00
nym21 138b2bd357 release: v0.0.110 2025-10-02 17:41:00 +02:00
nym21 16b14b1fe1 bitview: reorg part 10 + api changes 2025-10-02 17:40:23 +02:00
nym21 c4ce718bb2 bitview: reorg part 9 2025-10-01 23:17:48 +02:00
nym21 62d4b35c93 bitview: reorg part 8 2025-09-29 14:17:49 +02:00
nym21 7407c032e5 bitview: reorg part 7 + fix hanging ? 2025-09-28 20:33:55 +02:00
nym21 9d03fdf31d bitview: reorg part 6 2025-09-27 19:52:11 +02:00
nym21 dfe5148f17 bitview: reorg part 5 2025-09-26 00:04:14 +02:00
nym21 0d5b792c57 bitview: reorg part 4 + remove breakeven metrics 2025-09-24 23:58:41 +02:00
nym21 2279aa8f18 bitview: reorg part 3 2025-09-24 00:35:32 +02:00
nym21 d45686128e bitview: reorg part 2 2025-09-23 19:58:34 +02:00
nym21 5b6ce5d8ee bitview: reorg part 1 2025-09-22 18:43:53 +02:00
nym21 aad34c4d52 websites: restructured 2025-09-21 17:22:48 +02:00
nym21 470082cc65 websites: restructured 2025-09-21 17:21:10 +02:00
nym21 6554f35710 changelog: update 2025-09-20 23:33:39 +02:00
nym21 335fe24a54 changelog: update 2025-09-20 19:44:57 +02:00
nym21 3831ef7b25 release: v0.0.109 2025-09-20 19:20:40 +02:00
nym21 8127337a09 cargo: update deps 2025-09-20 19:20:21 +02:00
nym21 9a59c2e541 release: v0.0.108 2025-09-20 18:43:53 +02:00
nym21 27adca5653 brk: fix readme in cargo.toml 2025-09-20 18:43:43 +02:00
nym21 2c5b502da9 global: serialization optimizations for faster responses 2025-09-20 18:42:15 +02:00
nym21 23f6397a97 computer: blk metadata fixes 2025-09-19 16:45:57 +02:00
nym21 43117825d7 computer: add positions 2025-09-18 19:45:16 +02:00
nym21 cc5701ea62 parser: rework, made stateless 2025-09-17 23:31:57 +02:00
nym21 9524eafea1 api: fix crashes on invalid addr/txid 2025-09-17 11:48:40 +02:00
nym21 c28a0f96f7 structs: fix locktime conversion to bitcoin::locktime 2025-09-17 11:38:38 +02:00
nym21 301dee96dc readmes: regenerated 2025-09-16 22:01:55 +02:00
nym21 185fc7b6ed changelog: update + claude: prompts 2025-09-16 16:30:44 +02:00
nym21 6d194dbb71 bitview: fix top unit + add back lib types 2025-09-16 15:33:05 +02:00
nym21 d34f4bdd12 changelog: update 2025-09-15 18:39:48 +02:00
nym21 17dc4bde5e global: snapshot 2025-09-14 23:13:18 +02:00
nym21 ce50b14591 tood: update 2025-09-14 14:41:37 +02:00
nym21 f7bd319954 project: cleanup root 2025-09-13 18:32:13 +02:00
nym21 e9c0121a18 release: v0.0.107 2025-09-13 18:27:45 +02:00
nym21 01aa425f81 global: chain + cointime datasets 2025-09-13 18:26:28 +02:00
nym21 38d5c7dff6 computer: add tx annualized volume + tx velocity + rename _in_usd/_in_btc to _usd/_btc 2025-09-13 00:29:34 +02:00
nym21 e3b4b9b618 computer: some cleanup 2025-09-12 12:07:04 +02:00
nym21 a5951c58f3 global: add sent volume 2025-09-12 12:00:03 +02:00
nym21 504d6eaa9f cargo: update 2025-09-11 22:53:16 +02:00
nym21 6253fa30ef global: more mining related datasets 2025-09-11 18:45:54 +02:00
nym21 47f7cef4f4 global: add hash related datasets 2025-09-11 01:02:29 +02:00
nym21 72bba06e71 global: add mining related datasets 2025-09-10 21:57:15 +02:00
nym21 9b92c5ce38 computer: convert vecs functions to iterators 2025-09-10 16:25:38 +02:00
nym21 dfa077a1c9 computer: simplify compute_all functions 2025-09-09 19:22:56 +02:00
nym21 18fb2e7d4d release: v0.0.106 2025-09-09 17:53:09 +02:00
nym21 a610fd53e2 global: add min max choppiness datasets + fixes 2025-09-09 17:52:45 +02:00
nym21 16abce1f2d release: v0.0.105 2025-09-08 20:16:38 +02:00
nym21 f3b42f34a6 dist: add config back to config.toml 2025-09-08 20:16:21 +02:00
nym21 6483d324de release: v0.0.104 2025-09-08 20:02:18 +02:00
nym21 5ab97050dd ci: udpate dist + release.yml 2025-09-08 20:01:51 +02:00
nym21 17eed70903 release: v0.0.103 2025-09-08 19:24:00 +02:00
nym21 88067c03b7 release: v0.0.102 2025-09-08 19:21:43 +02:00
nym21 7c1e5b913f cargo: update 2025-09-08 19:20:53 +02:00
nym21 0014235e91 global: add price volatility datasets 2025-09-08 18:24:22 +02:00
nym21 a39b7be1d1 release: v0.0.101 2025-09-07 21:55:56 +02:00
nym21 de98c5f706 global: fixes 2025-09-07 21:55:39 +02:00
nym21 10b496e845 release: v0.0.100 2025-09-07 17:14:10 +02:00
nym21 bbe7bf390d crates: upgrade rest 2025-09-07 17:13:57 +02:00
nym21 4777b3400a crates: upgrade seqdb 2025-09-07 17:13:01 +02:00
nym21 acaa70e944 release: v0.0.99 2025-09-07 17:01:58 +02:00
nym21 4049d694f7 global: snapshot + pools + fixes 2025-09-07 17:01:34 +02:00
nym21 e155a3dacf bitview: fix localstorage error 2025-09-06 15:41:16 +02:00
nym21 a224e4c4d8 release: v0.0.98 2025-09-05 14:50:46 +02:00
nym21 edaeda5424 release: v0.0.97 2025-09-05 14:47:35 +02:00
nym21 09d974913d computer: pools part 1 + fetcher: fix url + interface: more ddos protection 2025-09-05 14:47:11 +02:00
nym21 f82edb290a global: add datasets and charts 2025-09-05 10:00:29 +02:00
nym21 3d8b33ae94 release: v0.0.96 2025-09-03 18:21:17 +02:00
nym21 565ecbd436 cargo: update 2025-09-03 18:20:58 +02:00
nym21 3359dfcc29 global: snapshot 2025-09-03 18:17:25 +02:00
nym21 1c2afd14dd global: fixes of Parser::new 2025-09-01 20:34:27 +02:00
nym21 fe5343c1d6 global: tiny snapshot 2025-09-01 20:21:51 +02:00
nym21 08cfefc02a zed: add project settings to improve search 2025-08-31 17:05:28 +02:00
nym21 f6d9332c48 bitview: fix screenshot in ios 2025-08-31 16:17:50 +02:00
nym21 cc6913c854 bitview: initial history support 2025-08-31 14:50:36 +02:00
nym21 8c75fbd0a4 server: fix urls in readme 2025-08-31 12:21:11 +02:00
nym21 0de6d62409 bitview: simplify options tree 2025-08-31 11:07:54 +02:00
nym21 5ba7ce5b7c bitview: small fixes 2025-08-30 12:11:15 +02:00
nym21 e106d30852 global: snapshot 2025-08-29 22:49:26 +02:00
nym21 30affc884b release: v0.0.95 2025-08-28 12:43:49 +02:00
nym21 745717ea49 global: added unrealized relative datasets 2025-08-28 12:43:28 +02:00
nym21 4efd98b758 release: v0.0.94 2025-08-28 00:31:36 +02:00
nym21 36640e3710 global: added datasets 2025-08-28 00:31:14 +02:00
nym21 311c4fd29d website: rename default to bitview 2025-08-27 11:52:22 +02:00
nym21 f50374f983 release: v0.0.93 2025-08-26 23:34:57 +02:00
nym21 82ceb7f021 cargo: update 2025-08-26 23:34:38 +02:00
nym21 0aba3bc1d8 release: v0.0.92 2025-08-26 22:27:16 +02:00
nym21 f6c984ff3c website: add screenshot feature 2025-08-26 22:26:55 +02:00
nym21 4091ab6b6c release: v0.0.91 2025-08-26 08:31:30 +02:00
nym21 fb9fd5b51a global: add datasets and charts + fixes 2025-08-26 08:31:08 +02:00
nym21 9389700a01 release: v0.0.90 2025-08-24 17:05:51 +02:00
nym21 016c1b2233 changelog: update 2025-08-24 17:05:35 +02:00
nym21 38b8a08297 release: v0.0.89 2025-08-24 16:46:54 +02:00
nym21 c9ffd3ad99 lock: update 2025-08-24 16:46:43 +02:00
nym21 61f960de28 global: snapshot 2025-08-24 16:45:20 +02:00
nym21 da1ff2cacc computer: stateful: maybe got rollback to work, tbd 2025-08-19 23:34:05 +02:00
nym21 05036c682f global: snapshot 2025-08-17 21:38:28 +02:00
nym21 7d47bc8042 changelog: add link to releases and to changes 2025-08-16 22:23:07 +02:00
nym21 98cfd160ef changelog: vibed 2025-08-16 19:16:09 +02:00
nym21 b5e3262b67 readmes: update 2025-08-16 18:21:44 +02:00
nym21 009fb35c4c computer: cleanup 2025-08-16 16:42:01 +02:00
nym21 8648d3131a computer: convert ComputedFrom to LazyFrom 2025-08-13 10:46:28 +02:00
nym21 00c316c35d readmes: vibed 2025-08-13 00:52:23 +02:00
nym21 5f8de8e756 computer: rollback part 1 2025-08-12 22:37:16 +02:00
nym21 ee5dc8fc41 computer: refactor 2025-08-10 16:00:44 +02:00
nym21 a61926988a release: v0.0.88 2025-08-10 14:16:31 +02:00
nym21 bd8c4dfb6b website: fix options 2025-08-10 14:16:13 +02:00
nym21 ce9b4bc4dd release: v0.0.87 2025-08-10 13:43:36 +02:00
nym21 8b12b00114 cargo: update deps 2025-08-10 13:43:17 +02:00
nym21 1775cc1d54 release: v0.0.86 2025-08-10 13:20:04 +02:00
nym21 e4bd09df24 lock: update crates 2025-08-10 13:19:52 +02:00
nym21 5e8c7da4df global: convert brk_vecs to its own crates and repo (seqdb/vecdb) + changes 2025-08-10 12:49:41 +02:00
nym21 c85592eefe release: v0.0.85 2025-08-07 22:35:09 +02:00
nym21 05861c9113 mcp: upgrade + made stateless 2025-08-07 22:34:46 +02:00
nym21 3508d1e315 release: v0.0.84 2025-08-07 21:23:19 +02:00
nym21 e3177b8054 global: per crate build.rs 2025-08-07 21:22:38 +02:00
nym21 03e3760152 global: snapshot + lock file + better errors 2025-08-07 17:29:30 +02:00
nym21 4740610923 global: compressed vecs work again 2025-08-05 23:38:43 +02:00
nym21 e28a0cde55 vecs: fix race condition 2025-08-04 23:48:20 +02:00
nym21 5b855fd835 global: snapshot 2025-08-04 11:38:46 +02:00
nym21 a2f5704581 global: snapshot 2025-08-03 23:38:58 +02:00
nym21 f7aa9424db global: one big snapshot 2025-08-02 16:59:22 +02:00
nym21 aa8b47a3dd global: cleanup 2025-07-29 17:46:30 +02:00
nym21 11911c1898 release: v0.0.83 2025-07-26 23:35:43 +02:00
nym21 4814c1971d vecs: add linux punch hole impl 2025-07-26 23:35:23 +02:00
nym21 be9569f3fb release: v0.0.82 2025-07-26 22:09:21 +02:00
nym21 900e72f95a cargo: cleanup deps 2025-07-26 14:28:26 +02:00
nym21 d2827f188b computer: temp remove rayon 2025-07-26 14:24:06 +02:00
nym21 cf9903b759 computer: init file with min length and regions 2025-07-26 08:57:13 +02:00
nym21 23f96461f4 computer: remove libc dep 2025-07-26 08:42:33 +02:00
nym21 9f2fd26e98 computer: fixes 2025-07-26 08:41:19 +02:00
nym21 78d837c080 computer: flush + punch 2025-07-26 01:04:36 +02:00
nym21 241b9312b7 cli: config changes 2025-07-26 00:46:35 +02:00
nym21 ed70ad7378 indexer: take readers before last export 2025-07-25 22:45:41 +02:00
nym21 00213176d8 indexer: small changes 2025-07-25 22:38:15 +02:00
nym21 406650a45a vec: removed 2025-07-25 20:38:57 +02:00
nym21 56750ccf3c vecs: part 11 2025-07-25 20:27:15 +02:00
nym21 dfc286b393 vecs: part 10 2025-07-25 20:22:54 +02:00
nym21 49a66f72fc crates: update rapidhash 2025-07-24 17:32:38 +02:00
nym21 3f237689da vecs: part 9 2025-07-24 17:19:05 +02:00
nym21 cf1fb483b3 vecs: part 8 2025-07-24 16:48:50 +02:00
nym21 b10f5e3f67 vecs: part 7 2025-07-23 23:55:13 +02:00
nym21 c4fc24c513 vecs: part 6 2025-07-23 09:17:26 +02:00
nym21 3ac9c2d95e vecs: part 5 2025-07-22 21:26:50 +02:00
nym21 e5ab4dafc0 vecs: part 4 2025-07-22 17:36:34 +02:00
nym21 10ae1911c3 vecs: part 3 2025-07-22 15:10:07 +02:00
nym21 73ebcdf0d6 vecs: part 2 2025-07-22 13:19:19 +02:00
nym21 5347523921 vecs: init 2025-07-21 11:02:25 +02:00
nym21 7ef70b953b vec: lazy: remove unneeded phantoms 2025-07-19 17:47:25 +02:00
nym21 ccaca524fe computer: libc sync 2025-07-19 10:10:01 +02:00
nym21 dd51f91cab computer: final fix for external disks crashing 2025-07-18 16:29:53 +02:00
nym21 537d98b41b release: v0.0.81 2025-07-17 23:45:01 +02:00
nym21 9c4cadfc04 vec: fix holes export 2025-07-17 17:29:53 +02:00
nym21 2001370441 mcp: use rust-rmcp instead of brk-rmcp 2025-07-17 16:34:29 +02:00
nym21 cc87b22757 computer: perf improvements 2025-07-17 16:17:21 +02:00
nym21 c0a65b30ad indexer: update example 2025-07-17 11:39:25 +02:00
nym21 c07e66c086 computer: fix stateful 2025-07-17 11:35:40 +02:00
nym21 a0cfc1be2b computer: convert stores to vecs part 2 2025-07-16 16:23:40 +02:00
nym21 1505454793 computer: convert stores to vecs part 1 2025-07-15 22:47:46 +02:00
nym21 e1dff66283 pr: merge #21 from deadmanoz/dockerize
Add Docker support
2025-07-15 15:51:32 +00:00
deadmanoz 5be801a086 Merge branch 'main' into dockerize 2025-07-15 08:50:22 -07:00
deadmanoz 94d4b05c29 Address review feedback 2025-07-15 08:48:39 -07:00
nym21 cebb889f7e cargo: update 2025-07-14 16:00:31 +02:00
nym21 c4ed6ed034 store: remove rotate_memtable as could be the root cause of the issue 2025-07-14 15:48:19 +02:00
nym21 ec960bfefa release: v0.0.80 2025-07-13 21:20:40 +02:00
nym21 79f689dde1 mcp: remove claude results examples due to dead links 2025-07-13 21:20:02 +02:00
nym21 3b3654df56 vec: add local and shared stored_len to raw variant 2025-07-13 19:30:50 +02:00
nym21 c66f008f07 release: v0.0.79 2025-07-13 17:18:14 +02:00
nym21 37d9498d90 crates: upgrade 2025-07-13 17:18:02 +02:00
nym21 1ff67093db website: apply datasets changes to charts 2025-07-13 17:14:34 +02:00
nym21 daed37ccb8 stores: forgot some files 2025-07-13 16:52:19 +02:00
nym21 d41d807b4f stores: bloom filters back to default due to slow reads, v3 will bring down the needed RAM 2025-07-13 16:49:45 +02:00
nym21 d6fa5c8a55 vec: fix header reading of existing file 2025-07-13 16:31:22 +02:00
nym21 2dd608dfed vec: don't store mmap in struct anymore 2025-07-13 11:50:34 +02:00
nym21 a98546f605 release: v0.0.78 2025-07-13 02:05:28 +02:00
nym21 3567559d4e release: v0.0.77 2025-07-13 01:54:51 +02:00
nym21 216476ee45 computer: reduce number of ratio datasets for some cohorts 2025-07-13 01:25:45 +02:00
nym21 3fc28c07fb computer: missed a file 2025-07-13 00:23:32 +02:00
nym21 85f6ef063d computer: perf again 2025-07-13 00:21:20 +02:00
nym21 1e71e2d68f computer: perf 2025-07-12 16:17:29 +02:00
nym21 b24a29895f global: perf + resource imprv 2025-07-12 15:07:02 +02:00
nym21 0167a2ae59 global: fixes 2025-07-12 11:18:51 +02:00
nym21 2c867103ca computer: remove dbg 2025-07-11 14:23:26 +02:00
nym21 8c289df336 computer: stateful: perf improvements 2025-07-11 11:43:53 +02:00
nym21 4489920cbf computer: fix coarse lazy indexes 2025-07-11 01:51:04 +02:00
nym21 029a85081b global: snapshot 2025-07-10 22:32:04 +02:00
nym21 1bc739d07f vec + comp: small changes 2025-07-10 18:35:54 +02:00
nym21 c229e218f6 global: adding semester + making coarser intervals computed instead of eager 2025-07-10 17:44:19 +02:00
nym21 a66f4ad4bd release: v0.0.76 2025-07-09 13:41:42 +02:00
nym21 1dd687dab7 bundler: upgrade rolldown dep 2025-07-09 13:41:31 +02:00
nym21 50ff6e2745 release: v0.0.75 2025-07-09 12:33:06 +02:00
nym21 811dec713b computer: reduce even more the number of par threads for compute_rest_part2 2025-07-09 12:32:50 +02:00
nym21 617d6f4bd7 release: v0.0.74 2025-07-09 11:51:24 +02:00
nym21 57cd2d6252 computer: fix par compute_rest_part2 crashing external drives 2025-07-09 11:48:41 +02:00
nym21 ec64f8d048 release: v0.0.73 2025-07-09 08:36:29 +02:00
nym21 ed288a9dba website: make panes the same size + remove saving their height 2025-07-09 00:06:38 +02:00
nym21 27da0a4102 packages: add lightweight-charts v5.0.8 2025-07-08 23:37:06 +02:00
nym21 3c01ba1a76 release: v0.0.72 2025-07-08 22:35:16 +02:00
nym21 252c8833ae global: upgrade deps 2025-07-08 22:34:51 +02:00
nym21 f45fb6efe6 global: renames and fixes 2025-07-08 21:33:18 +02:00
nym21 8cc1f8d691 computer: add more up to and from datasets 2025-07-07 23:53:59 +02:00
nym21 bff22b5182 websites: snapshot + todo: init 2025-07-07 13:16:43 +02:00
nym21 d31d47eb32 computer: store part 10 2025-07-05 17:44:51 +02:00
nym21 5fe984c39d indexer: rename some stores 2025-07-05 00:17:23 +02:00
nym21 7f07b0daa7 global: move addressindex_to_outputindex stores from computer to indexer 2025-07-04 17:30:43 +02:00
deadmanoz 5de9757d46 Remove services from docker 2025-07-04 16:37:39 +08:00
deadmanoz f89276d7b8 Remove redundant services 2025-07-04 15:51:28 +08:00
deadmanoz 30ba034206 Move docker artefacts into /docker directory 2025-07-04 13:00:12 +08:00
deadmanoz fa1e5aaa7f Make Parser::new the only entrypoint 2025-07-04 12:15:32 +08:00
deadmanoz 870c70180f Back to a single image/container setup 2025-07-04 11:40:37 +08:00
nym21 6d35c26b3f computer: store part 9 2025-07-03 19:15:02 +02:00
nym21 be4e693a27 computer: store part 8 2025-07-03 18:19:36 +02:00
nym21 5810276156 computer: store part 7 2025-07-02 23:49:24 +02:00
nym21 d10ac3f87b computer: store part 6 2025-07-02 16:02:18 +02:00
nym21 9810bc09e9 computer: store part 5 2025-07-01 20:03:00 +02:00
nym21 a0a13eb2a8 computer: store part 4 2025-07-01 13:57:48 +02:00
nym21 6e996797b8 computer: store part 3 2025-06-29 17:39:31 +02:00
nym21 663092b501 global: replace Value enum with Cow 2025-06-27 20:39:19 +02:00
nym21 8ea13544de computer: store part 2 2025-06-27 19:38:44 +02:00
nym21 e73daa6214 computer: init store 2025-06-27 10:52:36 +02:00
deadmanoz d83a833b4d Switch to multiple container setup 2025-06-27 12:56:25 +08:00
deadmanoz ec3a2f29f0 Docker functionality, change location of 'blk_index_to_blk_recap.json' 2025-06-27 12:56:03 +08:00
nym21 cf92c60a01 mcp: readme 2025-06-26 17:41:00 +02:00
nym21 b7f51b03bc global: snapshot 2025-06-26 16:40:29 +02:00
nym21 903e69ff77 release: v0.0.71 2025-06-25 17:59:19 +02:00
nym21 c4167ddaad mcp: small changes 2025-06-25 10:44:24 +02:00
nym21 50bfdb0d68 release: v0.0.70 2025-06-24 23:53:30 +02:00
nym21 a6cb09ff1c fetcher: fix brk url 2025-06-24 23:53:10 +02:00
nym21 e4c9f23476 release: v0.0.69 2025-06-24 13:23:14 +02:00
nym21 44e5415d43 vec: undo storing file in struct, can overwhelm system 2025-06-24 13:20:44 +02:00
nym21 1c653693ed release: v0.0.68 2025-06-24 12:19:07 +02:00
nym21 39c470ad7a core: increase max open files limit 2025-06-24 12:18:49 +02:00
nym21 1103e538a5 release: v0.0.67 2025-06-24 11:57:25 +02:00
nym21 c0cd4cba6f global: snapshot 2025-06-24 11:56:54 +02:00
nym21 b91120e8d4 readme: update 2025-06-24 08:42:40 +00:00
nym21 005774a4c2 mcp: update readme 2025-06-24 08:41:08 +00:00
nym21 16bbfebfba changelog: update titles 2025-06-24 08:26:01 +00:00
nym21 15505cd82d web: fix index type imports 2025-06-24 10:23:16 +02:00
nym21 016d80e002 server: use etag for vecs instead of date modified 2025-06-24 10:11:03 +02:00
nym21 0f3c267a48 global: rename some indexes 2025-06-23 22:00:09 +02:00
nym21 589bb02411 vec: single file with header 2025-06-23 20:48:00 +02:00
nym21 c0f4ece17b mcp: part 2 2025-06-21 20:34:14 +02:00
nym21 c3ae3cb768 server: mcp + global: refactor 2025-06-21 12:43:14 +02:00
nym21 c9e0f9d985 query: remove dup 'h' short index 2025-06-19 14:16:24 +02:00
nym21 e3431c2fa3 release: v0.0.66 2025-06-19 12:32:04 +02:00
nym21 5979b9771e global: cointime part 1 2025-06-19 12:29:34 +02:00
nym21 aa61832fb2 web: fix options cmumulative possible vecids type 2025-06-18 10:14:09 +02:00
nym21 2ac6e982b1 computer: cumulative destroyed coinblocks and cointime 2025-06-18 10:13:33 +02:00
nym21 3204ddcf07 pr: merge #18 from StevenBlack/typos
Minor refinements to the brk_cli readme
2025-06-17 23:06:32 +02:00
nym21 c87b1c133c release: v0.0.65 2025-06-17 22:30:32 +02:00
nym21 9b275ecdae web: fix hang when candles fetched are slightly diff than before 2025-06-17 22:30:11 +02:00
Steven Black d6fd7de361 Minor refinements to the brk_cli readme
* Fix typos
* Rephrasing some descriptions
* Links to run command parameters now link to the code as it is in blob  49d66a133e specifically.
* Lightly introduce some nice features of Github flavored markdown (IMPORTANT and TIP) to assess how this nuance is received by maintainers.
2025-06-17 14:07:03 -04:00
nym21 49d66a133e release: v0.0.64 2025-06-17 18:49:17 +02:00
nym21 c559f26d0e global: add a bunch of realized datasets + charts 2025-06-17 18:47:04 +02:00
nym21 bbe9f1bad2 global: 4y zscore + 200d sma + mayer's multiple 2025-06-17 10:01:49 +02:00
nym21 7e1fb6472d release: v0.0.63 2025-06-16 23:46:57 +02:00
nym21 0ff8d20573 web: fix the fix for the stutter + pwa assets 2025-06-16 23:46:39 +02:00
nym21 9c1f9448dc release: v0.0.62 2025-06-16 18:56:59 +02:00
nym21 43a6081dd6 web: fix stutter on update and save default chart settings to url params 2025-06-16 18:56:38 +02:00
nym21 985e961876 web: fix error in lockdown safari + charts: update instead of setData when possible 2025-06-16 18:20:56 +02:00
nym21 098f6de047 release: v0.0.61 2025-06-15 17:30:49 +02:00
nym21 1b0f90fd68 release: v0.0.60 2025-06-15 17:27:41 +02:00
nym21 12252f407b computer: fix open of ohlc if fetched from different API than prev ohlc 2025-06-15 17:27:16 +02:00
nym21 3b6e3f47ab release: v0.0.59 2025-06-15 12:40:46 +02:00
nym21 6a9ac9b025 brk: fix bundler use + bundler: remove minify html crate 2025-06-15 12:39:50 +02:00
nym21 ae6aa4088b release: v0.0.58 2025-06-15 01:50:22 +02:00
nym21 c08f431180 bundler: deploy brk_rolldown + fix edge case 2025-06-15 01:50:01 +02:00
nym21 123c1f56e9 release: v0.0.57 2025-06-14 22:47:57 +02:00
nym21 35ac65a864 server: update cache control for bundled websites 2025-06-14 22:47:26 +02:00
nym21 e9f362cc87 bundler: init working version 2025-06-14 20:17:49 +02:00
nym21 65685c23e1 release: v0.0.56 2025-06-13 18:03:28 +02:00
nym21 2f74748cea computer: stateful: reset when reorg detected 2025-06-13 18:03:09 +02:00
nym21 f477bd66f3 release: v0.0.55 2025-06-13 10:23:38 +02:00
nym21 d7d77ae8f0 global: multiple fixes 2025-06-13 10:22:03 +02:00
nym21 31110a740d release: v0.0.54 2025-06-12 22:18:36 +02:00
nym21 b64d8b1d7f release: v0.0.53 2025-06-12 22:16:33 +02:00
nym21 c46006aacc web: filter possible index choices in charts 2025-06-12 22:09:33 +02:00
nym21 92f81b1493 web: fix css 2025-06-12 20:23:23 +02:00
nym21 70213cfc8f websites: default: add auto price series type 2025-06-12 18:41:56 +02:00
nym21 8a82bf5c50 websites: default: add live price 2025-06-12 18:10:24 +02:00
nym21 37405384a2 vec: fixed compressed, still slow par read, cli: made raw the default 2025-06-12 16:31:54 +02:00
nym21 54ea6cc53b indexer: only raw format + global: fixes 2025-06-12 12:33:43 +02:00
nym21 339c00d815 release: v0.0.52 2025-06-11 21:19:41 +02:00
nym21 ea6b4dcde2 websites: default: remove scrollToSelected 2025-06-11 21:19:22 +02:00
nym21 2b84623d1e release: v0.0.51 2025-06-11 21:09:07 +02:00
nym21 c8b3afa56b websites: default: fix sw adn co 2025-06-11 21:08:42 +02:00
nym21 1348f3c24c release: v0.0.50 2025-06-11 18:11:22 +02:00
nym21 62208ce3e1 websites: default: fix minBarSpacing 2025-06-11 18:11:11 +02:00
nym21 813b2481de release: v0.0.49 2025-06-11 17:51:31 +02:00
nym21 27b924ba61 cargo: set full version of crates 2025-06-11 17:51:11 +02:00
nym21 b40170b8ce websites: default: snapshot 2025-06-11 17:45:17 +02:00
nym21 8bfa9d2734 websites: default: snapshot 2025-06-11 11:25:25 +02:00
nym21 c7cf76d4a8 websites: default: snapshot 2025-06-10 18:54:18 +02:00
nym21 dfd2969b3e websites: default: snapshot 2025-06-09 17:58:26 +02:00
nym21 0e1866fe1d release: v0.0.48 2025-06-09 13:53:33 +02:00
nym21 b9ae46b913 readme: update 2025-06-09 13:53:09 +02:00
nym21 06e7284055 websites: default: snapshot 2025-06-09 13:05:03 +02:00
nym21 93289e8fca release: v0.0.47 2025-06-08 20:35:36 +02:00
nym21 130d5057d4 server: readme: add index-t-value documentation 2025-06-08 20:35:26 +02:00
nym21 be492d5084 server: add support for /api/X-to-Y + fix query cli + add meta api endpoints 2025-06-08 20:30:53 +02:00
nym21 e0bf1d736f query: add count param 2025-06-08 18:26:59 +02:00
nym21 5a6b71cbeb server: add ddos protection 2025-06-08 17:06:36 +02:00
nym21 e6934cd5e2 release: v0.0.46 2025-06-08 16:06:27 +02:00
nym21 b5aada0792 websites: mv sw to root 2025-06-08 16:05:21 +02:00
nym21 165ea83ac3 websites: update service worker 2025-06-08 13:03:37 +02:00
nym21 440a82dee4 release: v0.0.45 2025-06-08 09:11:31 +02:00
nym21 9c2d3e5e26 server: fix existing folder endpoints 2025-06-08 09:10:55 +02:00
nym21 6fb6abcbe5 release: v0.0.44 2025-06-07 18:53:51 +02:00
nym21 dc449dafd1 websites: default: up deps + fix css 2025-06-07 18:53:34 +02:00
nym21 ecdaeebbfb release: v0.0.43 2025-06-07 13:48:25 +02:00
nym21 fa958b59bd fetcher: support new api 2025-06-07 13:48:07 +02:00
nym21 fb3d8521cd computer: coinblocks fix overflow 2025-06-07 13:29:08 +02:00
nym21 608c401cf3 release: v0.0.42 2025-06-07 10:40:32 +02:00
nym21 1c3da90a24 release: v0.0.41 2025-06-07 10:31:36 +02:00
nym21 34567f3375 changelog: reset last 2025-06-07 10:31:07 +02:00
nym21 51bcbeb48f global: multiple fixes 2025-06-07 09:30:42 +02:00
nym21 cc0f9c42df global: snapshot 2025-06-06 16:08:20 +02:00
nym21 a11bf5523b global: wip 2025-06-06 12:23:45 +02:00
nym21 1921c3d901 global: wip 2025-06-06 10:46:38 +02:00
nym21 d568469e8b global: works but data is wrong 2025-06-04 17:01:16 +02:00
nym21 20d5c7e8d5 global: wip + fixed eager mode 2025-06-03 17:49:20 +02:00
nym21 9f289ed9de global: wip 2025-06-03 10:11:51 +02:00
nym21 93ee5e480b global: wip 2025-06-02 18:22:42 +02:00
nym21 98a312701f computer: more frequent flushes 2025-06-01 16:11:13 +02:00
nym21 cbcf603b63 global: wip 2025-06-01 14:37:19 +02:00
nym21 f976f672cf global: wip 2025-05-31 20:45:59 +02:00
nym21 cfc3081e8a global: snapshot 2025-05-29 10:39:58 +02:00
nym21 99818924ee global: snapshot 2025-05-28 16:53:18 +02:00
nym21 9bbf3a027f global: snapshot 2025-05-28 15:42:55 +02:00
nym21 93e01902e3 global: snapshot 2025-05-27 15:19:53 +02:00
nym21 34919aba05 global: versions 2025-05-26 11:34:37 +02:00
nym21 a8ee4cf57f release: v0.0.40 2025-05-25 13:38:48 +02:00
nym21 b39548b4c6 core: fix eq and cmp of float structs 2025-05-25 12:35:52 +02:00
nym21 4217c22ff6 global: utxos part 8 2025-05-25 00:27:18 +02:00
nym21 4ab10670c9 global: utxos part 7 2025-05-24 12:52:15 +02:00
nym21 2883f88de6 global: utxos part 6 2025-05-23 17:52:01 +02:00
nym21 e002a61a19 global: utxos part 5 2025-05-22 19:04:55 +02:00
nym21 5893376279 global: utxos part 4 2025-05-19 17:53:09 +02:00
nym21 411c5e4c4d global: snapshot 2025-05-18 17:28:09 +02:00
nym21 c2a77072d2 global: utxos part 3 2025-05-18 11:52:14 +02:00
nym21 c8a25934a6 global: utxos part 2 2025-05-17 19:51:52 +02:00
nym21 7b38355cd4 release: v0.0.39 2025-05-16 23:37:51 +02:00
nym21 ddc54e0b98 release: v0.0.38 2025-05-16 23:34:32 +02:00
nym21 8a7003782b global: utxos dataset part 1 2025-05-16 23:33:19 +02:00
nym21 8e6464dacb release: v0.0.37 2025-05-14 11:28:38 +02:00
nym21 92b1dc0afb global: dca classes 2025-05-14 11:28:18 +02:00
nym21 7562f51e07 release: v0.0.36 2025-05-13 13:01:32 +02:00
nym21 09bba99e68 kibo: add priceline 2025-05-13 13:01:11 +02:00
nym21 9d674cd49b global: snapshot 2025-05-13 11:46:03 +02:00
nym21 88a0c9ea03 global: returns (lump sum vs dca) 2025-05-13 01:27:21 +02:00
nym21 5014e0ce3e release: v0.0.35 2025-05-12 12:56:08 +02:00
nym21 b7a1ee9ebc global: averages + ratio datasets 2025-05-12 12:55:40 +02:00
nym21 292ceddd66 comp + kibo: add market smas 2025-05-10 13:17:51 +02:00
nym21 4b52b80000 release: v0.0.34 2025-05-09 21:35:07 +02:00
nym21 9f20664c6e global: add some market charts 2025-05-09 16:04:54 +02:00
nym21 851a6aac0e release: v0.0.33 2025-05-08 12:53:19 +02:00
nym21 1f1e73c47a vec: computed: fix eager path + delete path if lazy 2025-05-08 12:31:31 +02:00
nym21 112f61ca18 release: v0.0.32 2025-05-08 11:17:35 +02:00
nym21 96eeacbe2b lazy: done 2025-05-08 11:15:47 +02:00
nym21 3f62da879c comp + lazy: part 6 2025-05-06 12:23:37 +02:00
nym21 aa30feb875 comp + vec: snapshot before bug hunting 2025-05-06 00:44:39 +02:00
nym21 9ba3c2b7c5 global: big vec refactor + lazy 2025-05-05 12:47:52 +02:00
nym21 320c708e10 computer: lazy part 4 2025-05-03 17:28:48 +02:00
nym21 efa7294f59 computer: lazy part 3 2025-05-03 11:44:33 +02:00
nym21 ae0e092935 computer: lazy part 2 2025-05-01 20:52:39 +02:00
nym21 c77aecbfce computer: lazy part 1 2025-05-01 17:25:59 +02:00
nym21 700352ec45 vec: caching only in iter 2025-04-30 18:29:18 +02:00
nym21 664b125ce2 release: v0.0.31 2025-04-30 01:12:28 +02:00
nym21 5f4b1c9e32 global: fixes 2025-04-30 01:11:42 +02:00
nym21 d11d3f19bd fix: old X links that now directed to impersonater 2025-04-29 15:04:59 +02:00
nym21 f34f4f2738 computer: remove last indexes 2025-04-29 15:02:41 +02:00
nym21 15db7c2310 computer: use count instead of last_index 2025-04-29 11:33:14 +02:00
nym21 f9257ed04d global: vec iter part 2 2025-04-28 18:30:11 +02:00
nym21 15e6ef8488 computer: remove the need for &mut vecs 2025-04-28 11:21:28 +02:00
nym21 9ae0a57f22 global: snapshot 2025-04-27 16:29:21 +02:00
nym21 1e38c21f8e vec: make collect_range use into_iter 2025-04-27 10:39:14 +02:00
nym21 bdc3c19163 vec: iter + global: snapshot 2025-04-27 00:21:21 +02:00
nym21 d55478da54 kibo: remove old packages versions 2025-04-26 17:28:41 +02:00
nym21 82bcc55645 global: fixes + snapshot + packages 2025-04-26 17:22:58 +02:00
nym21 07618ebe43 global: renames + refactor + p2a support 2025-04-25 18:16:23 +02:00
nym21 1492834d1e fjall: use a single keyspace for all stores + core: locktime -> rawlocktime 2025-04-24 15:59:13 +02:00
nym21 5ab6197356 computer: init lazy/eager 2025-04-23 22:36:10 +02:00
nym21 0a789fe551 release: v0.0.30 2025-04-23 00:07:41 +02:00
nym21 caa8ff23ed kibo: fix charts data fetch 2025-04-23 00:04:09 +02:00
nym21 ee30d1d36d release: v0.0.29 2025-04-22 19:34:55 +02:00
nym21 0d9415db9d kibo: fix add ts-ignore 2025-04-22 19:33:57 +02:00
nym21 8020e1126f kibo: database: part 2 2025-04-22 19:31:30 +02:00
nym21 3439422057 kibo: database: part 1 2025-04-21 23:17:37 +02:00
nym21 68d2bf736f release: v0.0.28 2025-04-19 11:46:05 +02:00
nym21 d78c39fd8c computer + kibo: part 14 - fixes 2025-04-19 11:45:26 +02:00
nym21 b1dcad86b4 release: v0.0.27 2025-04-18 19:41:13 +02:00
nym21 9b6124074d dist: another fix 2025-04-18 19:40:09 +02:00
nym21 02cbaa1e80 release: v0.0.26 2025-04-18 19:00:19 +02:00
nym21 a12f1321c7 dist: fix format tag version 2025-04-18 18:59:53 +02:00
nym21 8b67f592ac release: v0.0.25 2025-04-18 18:48:10 +02:00
nym21 319d17b337 dist: fix ubuntu version 2025-04-18 18:45:27 +02:00
nym21 476eaa85da github: add manual trigger 2025-04-18 18:37:51 +02:00
nym21 d26099855c dist: update ubuntu version 2025-04-18 18:36:18 +02:00
nym21 e47456da17 release: v0.0.24 2025-04-18 18:22:23 +02:00
nym21 a464d5d0b6 crates: upgrade 2025-04-18 18:20:40 +02:00
nym21 1cfb7b5615 computer + kibo: part 13 2025-04-18 18:08:11 +02:00
nym21 ac7c2f3d03 kibo: cleanup 2025-04-17 17:06:44 +02:00
nym21 638d9e6e01 computer: part 12 2025-04-17 15:22:34 +02:00
nym21 8b9df2a396 parser: readme 2025-04-15 14:40:56 +02:00
nym21 d7fe911bde parser: readme 2025-04-15 11:56:50 +02:00
nym21 0acc3d511b parser: readme 2025-04-15 11:27:48 +02:00
nym21 4cf465f419 computer: unwrap 2025-04-15 11:18:31 +02:00
nym21 b686d317a9 release: v0.0.23 2025-04-14 22:22:19 +02:00
nym21 dcef541852 release: v0.0.22 2025-04-14 21:57:15 +02:00
nym21 abdd733f11 deps: upgrade 2025-04-14 21:56:47 +02:00
nym21 942431e882 computer + kibo: part 11 2025-04-14 16:27:22 +02:00
nym21 1c75ea046c computer + kibo: part 10 2025-04-11 19:21:35 +02:00
nym21 f32b6daa51 release: v0.0.21 2025-04-11 12:01:30 +02:00
nym21 3736d6ba5e computer + kibo: part 9 2025-04-11 12:00:26 +02:00
nym21 9788b01f35 readmes: update discord link yet again 2025-04-10 22:57:09 +02:00
nym21 9aec991da6 release: v0.0.20 2025-04-10 21:39:34 +02:00
nym21 910701ce04 cleanup: old files 2025-04-10 21:39:12 +02:00
nym21 34b462d511 global: snapshot 2025-04-10 21:38:39 +02:00
nym21 139e93b2f0 vec: rework part 4 2025-04-10 15:55:26 +02:00
nym21 0dd7e9359e vec: rework part 3 2025-04-10 01:11:52 +02:00
nym21 41cf0225e3 vec: rework part 2 2025-04-09 22:59:18 +02:00
nym21 962254e511 vec: rework part 1 2025-04-09 16:31:31 +02:00
nym21 a7f2b24bac comp + vec: tiny opti 2025-04-08 15:38:20 +02:00
nym21 1323d988af computer + kibo: part 8 2025-04-08 11:40:35 +02:00
nym21 7c49e5c749 release: v0.0.19 2025-04-07 15:48:39 +02:00
nym21 cd69ec4fa3 computer: part 7 2025-04-07 15:48:00 +02:00
nym21 4c7e9fbee2 computer: part 6 2025-04-07 12:18:18 +02:00
nym21 1639df5616 computer: part 5 2025-04-06 12:01:45 +02:00
nym21 810cdbd790 chore: Release 2025-04-05 12:13:31 +02:00
nym21 0d4f4aec4e computer: part 4 2025-04-05 12:12:55 +02:00
nym21 6b1863d3b4 chore: Release 2025-04-05 00:58:50 +02:00
nym21 27f5a3b16b dist: move config to config.toml 2025-04-05 00:55:22 +02:00
nym21 876cd8291b disk: init 2025-04-05 00:33:31 +02:00
nym21 d0c46e4ef3 server: yet another fix + release: v0.0.16 2025-04-04 19:20:28 +02:00
nym21 feb8898ebf server: forgot a 'v' + release: v0.0.15 2025-04-04 18:59:49 +02:00
nym21 4fef8c5cfd release: v0.0.14 2025-04-04 18:49:41 +02:00
nym21 7d56d8e35b server: fix downloaded repo version path 2025-04-04 18:49:21 +02:00
nym21 5f1a3a9c8f release: v0.0.13 2025-04-04 18:31:31 +02:00
nym21 0767b3156d global: snapshot 2025-04-04 18:31:10 +02:00
nym21 9f16379b41 readme: add warning 2025-04-04 18:23:03 +02:00
nym21 be632aaf37 kibo: fix simulation 2025-04-04 17:44:22 +02:00
nym21 118c87faf7 kibo: snapshot 2025-04-04 11:30:59 +02:00
nym21 ec1e53d566 kibo: finished converting ts types to jsdoc 2025-04-04 10:54:44 +02:00
nym21 6a17ee414a kibo: move types around 2025-04-04 00:40:40 +02:00
nym21 6700686e4b website: signals: upgrade to tresshaked v0.2.4 2025-04-03 15:15:55 +02:00
nym21 e8c34dd59b global: snapshot 2025-04-03 14:31:39 +02:00
nym21 4c2da31bb3 server: version repo url when not in dev mode 2025-04-02 16:49:35 +02:00
nym21 c0144b99bf release: v0.0.12 2025-04-02 16:45:19 +02:00
nym21 a0c32fc146 server: api doc 2025-04-02 16:43:31 +02:00
nym21 a07b641adb global: snapshot 2025-04-02 16:20:17 +02:00
nym21 0bb869fb33 kibo: changed font from satoshi to geist 2025-04-01 23:46:25 +02:00
nym21 72389e0129 readmes: update 2025-04-01 00:24:27 +02:00
nym21 f49529fa70 release: v0.0.11 2025-03-31 18:52:39 +02:00
nym21 afcc34b5cc readmes: add shields 2025-03-31 18:52:16 +02:00
nym21 655b99cac8 server: add not_modified check when no 'to' param 2025-03-31 17:19:33 +02:00
nym21 cc5091e28c kibo: part 5 2025-03-31 17:17:34 +02:00
nym21 1c72362c6b kibo: part 4 2025-03-31 10:02:59 +02:00
nym21 50ad5f681b kibo: part 3 (broken) 2025-03-29 13:01:46 +01:00
nym21 50bf670931 kibo: part 2 (broken) 2025-03-25 20:55:47 +01:00
nym21 7a8896864f kibo: part 1.1 2025-03-21 17:17:04 +01:00
nym21 51fbf148d9 kibo: part 1 2025-03-21 16:59:39 +01:00
nym21 a9929438cd computer: part 3 2025-03-21 11:45:23 +01:00
nym21 5a94b6b56c cargo: update crates 2025-03-20 21:40:57 +01:00
nym21 52cfbf60d4 computer: part 2 2025-03-20 21:40:06 +01:00
nym21 29c10f8854 computer: part 1 2025-03-19 12:01:54 +01:00
nym21 ad761e388d READMEs: updated 2025-03-17 09:55:06 +01:00
nym21 07493ab0a6 README: add badges 2025-03-15 17:51:54 +01:00
nym21 7441011ae7 pr: merge #17 from aki-mizu/main
Merge pull request #17 from aki-mizu/main
fix old biter link in README
2025-03-15 15:18:32 +00:00
Darrell d0818f456d fix biter link 2025-03-16 00:10:25 +09:00
nym21 06ea07a021 release: v0.0.9 2025-03-14 20:55:21 +01:00
nym21 36d97ad5ca compression: added everywhere 2025-03-14 18:10:03 +01:00
nym21 a995eb2929 vec: compression part 2 and done 2025-03-14 16:00:47 +01:00
nym21 c459a3033d vec: compression part 1 2025-03-13 17:11:04 +01:00
nym21 b4fbcf6bee global: snapshot 2025-03-11 17:55:14 +01:00
nym21 b9e679a514 vec: moved compute functions to computer 2025-03-11 16:27:45 +01:00
nym21 64d73b93e4 global: snapshot 2025-03-11 15:36:40 +01:00
nym21 db70b05088 global: snapshot 2025-03-10 23:08:07 +01:00
nym21 9428beeae5 global: snapshot 2025-03-10 11:42:15 +01:00
nym21 f9f7172702 brk: use brk_cli::main as bin 2025-03-07 00:00:27 +01:00
nym21 f1851b304c server: server struct 2025-03-06 14:52:26 +01:00
nym21 d2ca6f1d46 server: use query for search 2025-03-05 16:55:37 +01:00
nym21 b27297cdc6 server: multiple frontends + auto download from github when needed 2025-03-05 12:22:11 +01:00
nym21 0d0edd7917 global: snapshot + core: impl Display for bytes structs 2025-03-04 12:29:19 +01:00
nym21 fc6f12fb22 general: fixed builds 2025-03-03 19:36:17 +01:00
nym21 d24096374f cli: add config for run command 2025-03-03 19:11:47 +01:00
nym21 4f1d04009a readme: update 2025-03-03 12:50:49 +01:00
nym21 79e9fde937 readme: updatre 2025-03-03 12:34:27 +01:00
nym21 0ebaf6a171 readme: update 2025-03-03 10:52:07 +01:00
nym21 be2012f28d cli + query: improvements 2025-03-02 20:15:01 +01:00
nym21 ceefc8ffc6 global: snapshot 2025-03-02 12:45:33 +01:00
nym21 0453b6903a vec: readme: removed example 2025-03-02 11:19:01 +01:00
nym21 691952249b readme: update 2025-03-02 11:08:53 +01:00
nym21 0ceae2852e query: init 2025-03-02 11:08:35 +01:00
nym21 6d7ff38cf2 global: snapshot 2025-03-01 15:22:34 +01:00
nym21 1b93ccf608 global: snapshot 2025-02-28 11:52:25 +01:00
nym21 5b1ca3711a kibo.money: cleaned packages 2025-02-27 12:34:45 +01:00
nym21 877f9299e1 global: snapshot 2025-02-27 12:32:54 +01:00
nym21 677aca7a03 indexer: rollback fixed via fjall v2.6.6 (conv on discord) 2025-02-27 00:50:35 +01:00
nym21 66b31a62d0 readme: add disclaimer 2025-02-26 12:59:35 +01:00
nym21 34923638c5 global: cargo update 2025-02-26 10:45:00 +01:00
nym21 bb61b3dc22 global: init readmes 2025-02-26 10:07:05 +01:00
nym21 01ecae8979 indexer: fixed Fjalls rollback 2025-02-24 18:15:13 +01:00
nym21 53175c9ed7 core: init 2025-02-24 14:08:51 +01:00
nym21 bc7a76755b git: removed old files 2025-02-24 00:53:57 +01:00
nym21 92758f3e4e Merge pull request #16 from bitcoinresearchkit/bkr-v0.0
Bkr v0.0
2025-02-23 23:47:39 +00:00
nym21 b09767c526 cargo: update descriptions 2025-02-24 00:43:50 +01:00
nym21 2f93fd7c36 global: crates cleanup 2025-02-24 00:25:58 +01:00
nym21 8acbcc548c parser: fixed hanging + global: snapshot 2025-02-23 21:53:39 +01:00
nym21 19cf34f9d4 brk: first commit 2025-02-23 01:25:15 +01:00
nym21 8c3f519016 iterator: add xor support 2025-02-22 14:06:43 +01:00
nym21 e63b42278c iterator: simplified 2025-02-21 20:12:57 +01:00
nym21 66ecd2fcf8 iterator: small changes 2025-02-20 13:11:23 +01:00
nym21 f0d86f2392 indexer: moved height to iterator 2025-02-20 11:40:26 +01:00
nym21 5e39510f21 general: snapshot 2025-02-19 21:43:18 +01:00
nym21 2cb4d65f3d indexer: update readme 2025-02-19 10:56:07 +01:00
nym21 15f2e05192 indexer: improved rollback; global: snapshot 2025-02-18 22:43:12 +01:00
nym21 a122333aaa indexer: rm canopy+sanakirja + init rollback; svec: added truncate 2025-02-15 12:04:20 +01:00
nym21 06b2186bf9 cargo: updated deps 2025-02-14 19:04:14 +01:00
nym21 ed10dccfe2 pricer: snapshot 2025-02-14 19:02:46 +01:00
nym21 a1006dddb5 global: snapshot 2025-02-13 19:00:52 +01:00
nym21 443a32dc81 server: cleanup 2025-02-13 11:10:34 +01:00
nym21 b034b4fe2f python: add read bytes from vec example 2025-02-12 17:22:36 +01:00
nym21 27b270148b server: add tsv support 2025-02-12 16:00:31 +01:00
nym21 269c64e4ed server: smart generic vec routes build 2025-02-12 14:11:04 +01:00
nym21 eaf76e27f5 struct_iterable: removed, seems not needed anymore 2025-02-12 00:06:46 +01:00
nym21 385b881068 server: added csv support to api 2025-02-11 23:38:01 +01:00
nym21 cf26696d12 global: snapshot 2025-02-11 19:01:30 +01:00
nym21 cb7ea40e7c server: started 2025-02-11 18:52:08 +01:00
nym21 5aaa55197e workspace: use folder name for packages 2025-02-05 23:42:48 +01:00
nym21 d86d614520 global: from custom unsafe_slice to zerocopy 2025-02-05 16:42:53 +01:00
nym21 138ca80c10 global: snapshot 2025-02-05 00:23:58 +01:00
nym21 d11a1622f8 storable_vec: add modes 2025-02-04 20:56:48 +01:00
nym21 42c996e16e global: snapshot 2025-02-02 23:55:05 +01:00
nym21 1e37d75e49 global: snapshot 2025-02-02 23:28:03 +01:00
nym21 ad34d9d402 bomputer: init 2025-01-31 11:43:14 +01:00
nym21 8c610f8a83 workspace: reorg 2025-01-28 17:45:36 +01:00
nym21 f7f3e3cc03 bindex: no copy fjall get + small fixes 2025-01-28 11:30:10 +01:00
nym21 d68c6f9f2e bindex: contained fjall code 2025-01-27 23:25:28 +01:00
nym21 90a5c4fbf8 bindex: retrying fjall 2025-01-27 12:49:19 +01:00
nym21 042be6e229 bindex: removed addressindex to in/out puts as they're computable later 2025-01-27 09:55:28 +01:00
nym21 4923c2e204 bindex: converted txindex_to_txoutindex_in to storable vecs 2025-01-26 17:42:47 +01:00
nym21 b94d94e116 bindex: snapshot 2025-01-25 22:46:11 +01:00
nym21 d629ae8fbb bindex: snapshot 2025-01-23 11:11:39 +01:00
nym21 1296a2e9ec general: massive update 2025-01-22 11:38:50 +01:00
nym21 009d02fa68 storable_vec: general update 2025-01-16 13:25:35 +01:00
nym21 4cc57e9c91 bindex: back to sanakirja 2025-01-14 23:35:42 +01:00
nym21 d373c6398e bitbase: renamed to bindex 2025-01-14 12:38:06 +01:00
nym21 82746a0669 bitbase: vecdisk 2025-01-14 12:32:27 +01:00
nym21 1212c3627b cargo: update fjall to v2.5 2025-01-09 20:53:04 +01:00
nym21 813f16ccee bitbase: small optimization 2025-01-09 00:13:52 +01:00
nym21 1c3cb91ecd bitbase: rayon done 2025-01-08 18:01:28 +01:00
nym21 5b1735db2b bitbase: pre-rayon snapshot 2025-01-07 12:15:58 +01:00
nym21 bf31ee5fd6 bitbase: move to transactional 2025-01-06 10:45:04 +01:00
nym21 1380b42c1d bitbase: snapshot 2025-01-03 21:34:10 +01:00
nym21 dea853d840 init: separate parser crate 2025-01-01 10:32:39 +01:00
nym21 d72bf0739a changelog: update 2024-12-27 12:33:09 +01:00
nym21 481f5c0a97 server: add support for dataset by timestamp 2024-12-27 12:28:27 +01:00
nym21 2b017ac6b5 website: lc: update to v4.2.2 2024-12-22 10:22:55 +01:00
nym21 8a733ee337 changelog: update 2024-12-22 09:39:27 +01:00
nym21 9dd87a48a6 server: rework api side 2024-12-22 00:42:11 +01:00
nym21 8fabbde13b global: small fixes 2024-12-17 10:39:28 +01:00
nym21 e0a378cb81 biter: fix ? 2024-12-14 17:39:42 +01:00
nym21 0b3329ca35 git: fix ignore file 2024-12-14 14:56:55 +01:00
nym21 50c77b51db snkrj: move database struct to its own crate 2024-12-14 14:55:44 +01:00
nym21 c883ed19d6 biter: readme: update 2024-12-13 19:57:08 +01:00
nym21 795791219e global: snapshot 2024-12-13 19:55:32 +01:00
nym21 f6f4660cd2 website: fix the previous fix 2024-12-13 16:08:59 +01:00
nym21 9576f6e91e website: fix window resize brave bug 2024-12-13 12:30:56 +01:00
nym21 f5e5bbefb2 changelog: update 2024-12-04 11:22:28 +01:00
nym21 d4323fb5e0 website: dca sim: improve reactivity 2024-12-04 11:15:18 +01:00
nym21 8af1ddd10d website: moved more code to lc wrapper 2024-12-04 10:28:30 +01:00
nym21 62f6d9a413 website: lc: moved markers to container 2024-12-03 19:04:21 +01:00
nym21 783aed5826 website: start containing lc code in wrapper 2024-12-03 17:31:56 +01:00
k 141cd819a1 website: reorg 2024-12-02 10:03:41 +01:00
k 44fa96eb49 website: sim: wording 2024-11-28 21:52:40 +01:00
k 778b514b65 website: simulation: fix sats added 2024-11-28 18:36:22 +01:00
k afd58d69e4 website: simulation: fix 'days ago' 2024-11-28 18:34:01 +01:00
k 4af9849b2b website: simulation: small changes 2024-11-28 18:31:15 +01:00
k 4dac44e720 website: simulation: small fixes 2024-11-28 15:58:11 +01:00
k 71871901ef website: update 2024-11-27 18:36:43 +01:00
k d39e7584c0 website: update 2024-11-27 12:56:04 +01:00
k 4e9c5612ca website: small fixes 2024-11-25 11:28:28 +01:00
k c8510dd45b changelog: update 2024-11-23 16:18:57 +01:00
k c234c17352 general: snapshot 2024-11-23 16:17:06 +01:00
k cfae483d9d parser: fix gnericmap multi_insert_simple_average 2024-11-20 11:42:06 +01:00
k d01ea13de4 global: snapshot 2024-11-20 10:50:14 +01:00
k 9a73ee6952 readme: fix link 2024-11-15 13:44:14 +01:00
k 28eb9e8c17 readme: update 2024-11-15 13:41:16 +01:00
k 749c91f662 readme: removed the fluff 2024-11-15 13:15:30 +01:00
k 97ac17a12a website: delete useless logs 2024-11-11 15:23:07 +01:00
k 32fd4fa8ed website: big update 2024-11-11 15:20:31 +01:00
k 12fe4c6ba5 parser: fix metadata bug 2024-11-08 22:53:39 +01:00
k b1e9fd95ca readme: typos 2024-11-05 09:36:49 +01:00
k d83043d8f2 server: readd content disposition attachement if ext present 2024-11-04 12:44:15 +01:00
k 2abeca6220 readme: update 2024-11-04 12:35:48 +01:00
k 781810ed9c parser: databases: small changes 2024-11-04 11:09:54 +01:00
k 2142847de3 website: moved packages + added ratio charts to compare folders 2024-11-02 12:42:40 +01:00
k ca42c266ef website: update cohorts colors 2024-11-02 00:59:57 +01:00
k f258ef1011 website: readd ratio to individual cohort folders 2024-11-01 20:52:43 +01:00
k 38cb763fd3 website: add compare to liquidity 2024-11-01 20:47:23 +01:00
k 3fa78241ef parser: exit inside global defrag 2024-11-01 20:23:06 +01:00
k 3c7bc13be9 Merge branch 'main' of github.com:kibo-money/kibo 2024-11-01 20:20:54 +01:00
k 2441ca35b3 website: up signals + added compare folder to all groups 2024-11-01 20:19:43 +01:00
k 216a3977be parser: reactivate 'first_defragment' option 2024-10-31 11:39:36 +01:00
k 647a51af15 parser: fix defrag 2024-10-31 09:57:06 +01:00
k 530d4ce717 general: temp rollback 2024-10-30 19:22:42 +01:00
k e5d81b4d5c parser: added databases defragmentation 2024-10-30 19:13:41 +01:00
k 6eaeca1f3d parser: db: improve iter function 2024-10-29 19:38:52 +01:00
k 4220034eab parser: crates upgrade && remove ram from config 2024-10-29 15:22:44 +01:00
k 76a8ddd354 server: tried oxc vs swc, kept swc 2024-10-29 14:40:12 +01:00
k 0bad38a815 iterable: added custom version 2024-10-28 16:51:20 +01:00
k 48a8aad20e parser: AnyDataset DX improvements 2024-10-28 16:48:27 +01:00
k 36ad0b3014 parser: revert save logic 2024-10-27 12:10:22 +01:00
k 95fc103eaf parser: fix metadata versioning 2024-10-27 10:52:42 +01:00
k f5754780a8 global: snapshot 2024-10-26 16:41:38 +02:00
k 7114c3bdf9 general: fixes 2024-10-21 14:36:02 +02:00
k 5b9d599e83 global: snapshot 2024-10-20 18:31:43 +02:00
k ffa4871035 readme: update instances 2024-10-19 11:49:01 +02:00
k 01832ac139 server: add .json option to last value routes 2024-10-19 11:04:32 +02:00
k cb7ff2bb37 changelog: update 2024-10-19 10:46:00 +02:00
k 35dd194b28 general: snapshot 2024-10-19 10:34:12 +02:00
k 7dac857135 docker: snapshot 2024-10-17 19:53:00 +02:00
k 608ccafc70 server: add support for .json .csv and ?all=true 2024-10-16 18:38:43 +02:00
k 4cdc9ef9b3 changelog: update 2024-10-09 00:39:25 +02:00
k db60d4e453 parser: compress empty_address_data 2024-10-09 00:33:14 +02:00
k f5d427a04f parser: cargo cleanup 2024-10-08 22:37:36 +02:00
k e4893e446c cargo: update 2024-10-08 21:53:38 +02:00
k 79ffbf3d1d global: snapshot 2024-10-08 21:47:46 +02:00
k 068bb07d6e global: snapshot 2024-10-04 19:09:09 +02:00
k 1c9d118ba2 changelog: update 2024-10-03 18:37:00 +02:00
k 5308796bac parser: removed liquidity split for everything but all addresses 2024-10-03 17:38:43 +02:00
k 669205aa4d general: snapshot 2024-10-02 10:48:05 +02:00
k 9d2c2f7945 global: snapshot 2024-09-29 20:39:51 +02:00
k e3b44b0adb website: refactor 2024-09-24 17:13:29 +02:00
k 1a303a9c38 docker: init 2024-09-23 18:44:55 +02:00
k 2befa58fce global: add sell side risk ratio 2024-09-21 14:22:27 +02:00
k c8ded4ddb3 general: add /api/last route 2024-09-20 16:56:36 +02:00
1740 changed files with 160913 additions and 40591 deletions
+2
View File
@@ -0,0 +1,2 @@
[build]
rustflags = ["-C", "target-cpu=native"]
+296
View File
@@ -0,0 +1,296 @@
# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist
#
# Copyright 2022-2024, axodotdev
# SPDX-License-Identifier: MIT or Apache-2.0
#
# CI that:
#
# * checks for a Git Tag that looks like a release
# * builds artifacts with dist (archives, installers, hashes)
# * uploads those artifacts to temporary workflow zip
# * on success, uploads the artifacts to a GitHub Release
#
# Note that the GitHub Release will be created with a generated
# title/body based on your changelogs.
name: Release
permissions:
"contents": "write"
# This task will run whenever you push a git tag that looks like a version
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
#
# If PACKAGE_NAME is specified, then the announcement will be for that
# package (erroring out if it doesn't have the given version or isn't dist-able).
#
# If PACKAGE_NAME isn't specified, then the announcement will be for all
# (dist-able) packages in the workspace with that version (this mode is
# intended for workspaces with only one dist-able package, or with all dist-able
# packages versioned/released in lockstep).
#
# If you push multiple tags at once, separate instances of this workflow will
# spin up, creating an independent announcement for each one. However, GitHub
# will hard limit this to 3 tags per commit, as it will assume more tags is a
# mistake.
#
# If there's a prerelease-style suffix to the version, then the release(s)
# will be marked as a prerelease.
on:
pull_request:
push:
tags:
- '**[0-9]+.[0-9]+.[0-9]+*'
jobs:
# Run 'dist plan' (or host) to determine what tasks we need to do
plan:
runs-on: "ubuntu-22.04"
outputs:
val: ${{ steps.plan.outputs.manifest }}
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
publishing: ${{ !github.event.pull_request }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
submodules: recursive
- name: Install dist
# we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.2/cargo-dist-installer.sh | sh"
- name: Cache dist
uses: actions/upload-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/dist
# sure would be cool if github gave us proper conditionals...
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
# functionality based on whether this is a pull_request, and whether it's from a fork.
# (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
# but also really annoying to build CI around when it needs secrets to work right.)
- id: plan
run: |
dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
echo "dist ran successfully"
cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@v4
with:
name: artifacts-plan-dist-manifest
path: plan-dist-manifest.json
# Build and packages all the platform-specific things
build-local-artifacts:
name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
# Let the initial task tell us to not run (currently very blunt)
needs:
- plan
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
strategy:
fail-fast: false
# Target platforms/runners are computed by dist in create-release.
# Each member of the matrix has the following arguments:
#
# - runner: the github runner
# - dist-args: cli flags to pass to dist
# - install-dist: expression to run to install dist on the runner
#
# Typically there will be:
# - 1 "global" task that builds universal installers
# - N "local" tasks that build each platform's binaries and platform-specific installers
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
runs-on: ${{ matrix.runner }}
container: ${{ matrix.container && matrix.container.image || null }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
steps:
- name: enable windows longpaths
run: |
git config --global core.longpaths true
- uses: actions/checkout@v4
with:
persist-credentials: false
submodules: recursive
- name: Install Rust non-interactively if not already installed
if: ${{ matrix.container }}
run: |
if ! command -v cargo > /dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
fi
- name: Install dist
run: ${{ matrix.install_dist.run }}
# Get the dist-manifest
- name: Fetch local artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- name: Install dependencies
run: |
${{ matrix.packages_install }}
- name: Build artifacts
run: |
# Actually do builds and make zips and whatnot
dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
echo "dist ran successfully"
- id: cargo-dist
name: Post-build
# We force bash here just because github makes it really hard to get values up
# to "real" actions without writing to env-vars, and writing to env-vars has
# inconsistent syntax between shell and powershell.
shell: bash
run: |
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@v4
with:
name: artifacts-build-local-${{ join(matrix.targets, '_') }}
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
# Build and package all the platform-agnostic(ish) things
build-global-artifacts:
needs:
- plan
- build-local-artifacts
runs-on: "ubuntu-22.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
- name: Fetch local artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- id: cargo-dist
shell: bash
run: |
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
echo "dist ran successfully"
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@v4
with:
name: artifacts-build-global
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
# Determines if we should publish/announce
host:
needs:
- plan
- build-local-artifacts
- build-global-artifacts
# Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine)
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: "ubuntu-22.04"
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Fetch artifacts from scratch-storage
- name: Fetch artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- id: host
shell: bash
run: |
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
echo "artifacts uploaded and released successfully"
cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@v4
with:
# Overwrite the previous copy
name: artifacts-dist-manifest
path: dist-manifest.json
# Create a GitHub Release while uploading all files to it
- name: "Download GitHub Artifacts"
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: artifacts
merge-multiple: true
- name: Cleanup
run: |
# Remove the granular manifests
rm -f artifacts/*-dist-manifest.json
- name: Create GitHub Release
env:
PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}"
ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}"
RELEASE_COMMIT: "${{ github.sha }}"
run: |
# Write and read notes from a file to avoid quoting breaking things
echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
announce:
needs:
- plan
- host
# use "always() && ..." to allow us to wait for all publish jobs while
# still allowing individual publish jobs to skip themselves (for prereleases).
# "host" however must run to completion, no skipping allowed!
if: ${{ always() && needs.host.result == 'success' }}
runs-on: "ubuntu-22.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
submodules: recursive
+29 -29
View File
@@ -1,46 +1,46 @@
# Mac OS
.DS_Store
# To do
/charts
TODO.md
# Builds
dist
target
# I/O
in
out
.log
/datasets
/price
# Sync
.stfolder
websites/dist
bridge/
/ids.txt
rust_out
# Copies
*\ copy*
# Ignored
ignore
_*
!__*.py
/*.md
/*.py
/*.json
/*.html
/research
/filter_*
# Scripts
/start-node.sh
# Logs
*.log*
# Editors
.vscode
.zed
# Environment variables/configs
.env
# Configs
config.toml
# Flamegraph
flamegraph/
# Profiling
profile.json.gz
flamegraph.svg
*.trace
# AI
.claude/settings*
# Expand
expand.rs
# Benchmarks
benches
[0-9]/
/benches
# Snapshots
snapshots*/
# AI
.claude
-231
View File
@@ -1,231 +0,0 @@
# Changelog
<!--
## v. 0.X.Y | WIP
![Image of the kibō Web App version 0.X.Y](./assets/v0.X.Y.jpg)
-->
## v. 0.4.1 | WIP
<!-- ![Image of the kibō Web App version 0.4.1](./assets/v0.4.1.jpg) -->
## Website
- Fixed service worker not passing 304 (not modified) response and instead serving cached responses
- Fixed history not being properly registered
- Fixed prices on charts not having a wide enough background due to the font not being fully loaded during the creation of the chart
- Fixed window being moveable on iOS when in standalone mode when it shouldn't be
## Server
- Fixed links in several places missing the `/api` part and thus not working
## v. 0.4.0 | [861950](https://mempool.space/block/00000000000000000000530d0e30ccf7deeace122dcc99f2668a06c6dad83629) - 2024/09/19
![Image of the kibō Web App version 0.4.0](./assets/v0.4.0.jpg)
### Brand
- **Satonomics** is now **kibō** 🎉
### Website
- Complete redesign of the website
- Rewrote the whole application and removed `node`/`npm`/`pnpm` dependencies in favor for pure `HTML`/`CSS`/`Javascript`
- Website is now served by the server
- Added Trading View attribution link to the settings frame and file in the lightweight charts folder
- Many other changes
### Parser
- Changed the block iterator from a custom version of [bitcoin-explorer](https://crates.io/crates/bitcoin-explorer) to the homemade [biter](https://crates.io/crates/biter) which allows the parser to run alongside `bitcoind`
- Added datasets compression thanks to [zstd](https://crates.io/crates/zstd) to reduce disk usage
- Use the Bitcoin RPC server for various calls instead of running cli commands and then parsing the JSON from the output
- **Important database changes that will need a full rescan**:
- Changed databases page size from 1MB to 4KB for improved disk usage
- Split txid_to_tx_data database in 4096 chunks (from 256) for improved disk usage
- Split address_index_to_X databases to chunks of 25_000 instead of 50_000
- Removed local Multisig database
- Updated the config, run with `-h` to see possible args
- Moved outputs from `/target/outputs` to `/out` to allow to run commands like `cargo clean` without side effects
- Various first run fixes
- Added to `-h` which arguments are saved, which is all of them at the time of writing
### Server
- Updated the code to support compressed binaries
- Added serving of the website
- Improved `Cache-Control` behavior
## v. 0.3.0 | [853930](https://mempool.space/block/00000000000000000002eb5e9a7950ca2d5d98bd1ed28fc9098aa630d417985d) - 2024/07/26
![Image of the Satonomics Web App version 0.3.0](./assets/v0.3.0.jpg)
### Parser
- Global
- Improved self-hosting by:
- Fixing an incredibly annoying bug that made the program panic because of a wrong utxo/address durable state after a or many new datasets were added/changed after a first successful parse of the chain
- Fixing a bug that would crash the program if launched for the first time ever
- Auto fetch prices from the main Satonomics instance if missing instead of only trying Kraken's and Binance's API which are limited to the last 16 hours
- Merged the core of `HeightMap` and `DateMap` structs into `GenericMap`
- Added `Height` struct and many others
- Reorganized outputs of both the parser and the server for ease of use and easier sync compatibility
- CLI
- Added an argument parser for improved UX with several options
- Datasets
- Added the following datasets for all entities:
- Value destroyed
- Value created
- Spent Output Profit Ratio (SOPR)
- Added the following ratio datasets and their variations to all prices {realized, moving average, any cointime, etc}:
- Market Price to {X}
- Market Price to {X} Ratio
- Market Price to {X} Ratio 1 Week SMA
- Market Price to {X} Ratio 1 Month SMA
- Market Price to {X} Ratio 1 Year SMA
- Market Price to {X} Ratio 1 Year SMA Momentum Oscillator
- Market Price to {X} Ratio 99th Percentile
- Market Price to {X} Ratio 99.5th Percentile
- Market Price to {X} Ratio 99.9th Percentile
- Market Price to {X} Ratio 1st Percentile
- Market Price to {X} Ratio 0.5th Percentile
- {X} 1% Top Probability
- {X} 0.5% Top Probability
- {X} 0.1% Top Probability
- {X} 1% Bottom Probability
- {X} 0.5% Bottom Probability
- {X} 0.1% Bottom Probability
- Added block metadatasets and their variants (raw/sum/average/min/max/percentiles):
- Block size
- Block weight
- Block VBytes
- Block interval
- Price
- Improved error message when price cannot be found
### App
- General
- Added chart scroll button for nice animations à la Wicked
- Added scale mode switch (Linear/Logarithmic) at the bottom right of all charts
- Added unit at the top left of all charts
- Added a backup API in case the main one fails or is offline
- Complete redesign of the datasets object
- Removed import of routes in JSON in favor for hardcoded typed routes in string format which resulted in:
- \+ A much lighter app
- \+ Better Lighthouse score
- \- Slower Typescript server
- Fixed datasets with null values crashing their fetch function
- Added a 'Go to a random chart' button in several places
- Chart
- Fixed series color being set to default ones after hovering the legend
- Fixed chart starting showing candlesticks and quickly switching to a line when it should've started directly with the line
- Separated the QRCode generator library from the main chunk and made it imported on click
- Fixed timescale changing on small screen after changing charts
- Folders
- Added the size in the "filename" of address cohorts grouped by size
- Favorites
- Added a 'favorite' and 'unfavorite' button at the bottom
- Settings
- Removed the horizontal scroll bar which was unintended
### Server
- Run file
- Only run with a watcher if `cargo watch` is available
- Removed id_to_path file in favor for only `paths.d.ts` in `app/src/types`
## v. 0.2.0 | [851286](https://mempool.space/block/0000000000000000000281ca7f1bf8c50702bfca168c7af1bdc67c977c1ac8ed) - 2024/07/08
![Image of the Satonomics Web App version 0.2.0](./assets/v0.2.0.jpg)
### App
- General
- Added the height version of all datasets and many optimizations to make them usable but only available on desktop and tablets for now
- Added a light theme
- Charts
- Added split panes in order to have the vertical axis visible for all datasets
- Added min and max values on the charts
- Fixed legend hovering on mobile not resetting on touch end
- Added "3 months" and yearly time scale setters (from year 2009 to today)
- Hide scrollbar of timescale setters and instead added scroll buttons to the legend only visible on desktop
- Improved Share/QR Code screen
- Changed all Area series to Line series
- Fixed horizontal scrollable legend not updating on preset change
- Performance
- Improved app's reactivity
- Added some chunk splitting for a faster initial load
- Global improvements that increased the Lighthouse's performance score
- Settings
- Finally made a proper component where you can chose the app's theme, between a moving or static background and its text opacity
- Added donations section with a leaderboard
- Added various links that are visible on the bottom side of the strip on desktop to mobile users
- Added install instructions when not installed for Apple users
- Misc
- Support mini window size, could be useful for embedded views
- Hopefully made scrollbars a little more subtle on WIndows and Linux, can't test
- Generale style updates
### Parser
- Fixed ulimit only being run in Mac OS instead of whenever the program is detected
## v. 0.1.1 | [849240](https://mempool.space/block/000000000000000000002b8653988655071c07bb5f7181c038f9326bc86db741) - 2024/06/24
![Image of the Satonomics Web App version 0.1.1](./assets/v0.1.1.jpg)
### Parser
- Fixed overflow in `Price` struct which caused many Realized Caps and Realized Prices to have completely bogus data
- Fixed Realized Cap computation which was using rounded prices instead normal ones
### Server
- Added the chunk, date and time of the request to the terminal logs
### App
- Chart
- Added double click option on a legend to toggle the visibility of all other series
- Added highlight effect to a legend by darkening the color of all the other series on the chart while hovering it with the mouse
- Added an API link in the legend for each dataset where applicable (when isn't generated locally)
- Save fullscreen preference in local storage and url
- Improved resize bar on desktop
- Changed resize button logo
- Changed the share button to visible on small screen too
- Improved share screen
- Fixed time range shifting not being the one in url params or saved in local storage
- Fixed time range shifting on series toggling via the legend
- Fixed time range shifting on fullscreen
- Fixed time range shifting on resize of the sidebar
- Set default view at first load to last 6 months
- Added some padding around the datasets (year 1970 to 2100)
- History
- Changed background for the sticky dates from blur to a solid color as it didn't appear properly in Firefox
- Build
- Tried to add lazy loads to have split chunks after build, to have much faster load times and they worked great ! But they completely broke Safari on iOS, we can't have nice things
- Removed many libraries and did some things manually instead to improve build size
- Strip
- Temporarily removed the Home button on the strip bar on desktop as there is no landing page yet
- Settings
- Added version
- PWA
- Fixed background update
- Changed update check frequency to 1 minute (~1kb to fetch every minute which is very reasonable)
- Added a nice banner to ask the user to install the update
- Misc
- Removed tracker even though it was a very privacy friendly as it appeared to not be working properly
### Price
- Deleted old price datasets and their backups
## v. 0.1.0 | [848642](https://mempool.space/block/000000000000000000020be5761d70751252219a9557f55e91ecdfb86c4e026a) - 2024/06/19
![Image of the Satonomics Web App version 0.1.0](./assets/v0.1.0.jpg)
## v. 0.0.X | [835444](https://mempool.space/block/000000000000000000009f93907a0dd83c080d5585cc7ec82c076d45f6d7c872) - 2024/03/20
![Image of the Satonomics Web App version 0.0.X](./assets/v0.0.X.jpg)
-8
View File
@@ -1,8 +0,0 @@
# Guidelines
## Parser
- Avoid floats as much as possible
- Use structs like `WAmount` and `Price` for calculations
- **Only** use `WAmount.to_btc()` when inserting or computing inside a dataset. It is **very** expensive.
- No `Arc`, `Rc`, `Mutex` even from third party libraries, they're slower
Generated
+3884
View File
File diff suppressed because it is too large Load Diff
+95
View File
@@ -0,0 +1,95 @@
[workspace]
resolver = "3"
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.4"
package.homepage = "https://bitcoinresearchkit.org"
package.repository = "https://github.com/bitcoinresearchkit/brk"
package.readme = "README.md"
[profile.dev]
lto = "thin"
codegen-units = 16
opt-level = 2
split-debuginfo = "unpacked"
[profile.release]
lto = "fat"
codegen-units = 1
panic = "abort"
strip = true
overflow-checks = false
[profile.bloaty]
debug = true
lto = false
strip = false
inherits = "release"
[profile.dist]
inherits = "release"
[profile.profiling]
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.4", path = "crates/brk_alloc" }
brk_bencher = { version = "0.1.0-alpha.4", path = "crates/brk_bencher" }
brk_bindgen = { version = "0.1.0-alpha.4", path = "crates/brk_bindgen" }
brk_cli = { version = "0.1.0-alpha.4", path = "crates/brk_cli" }
brk_client = { version = "0.1.0-alpha.4", path = "crates/brk_client" }
brk_cohort = { version = "0.1.0-alpha.4", path = "crates/brk_cohort" }
brk_computer = { version = "0.1.0-alpha.4", path = "crates/brk_computer" }
brk_error = { version = "0.1.0-alpha.4", path = "crates/brk_error" }
brk_fetcher = { version = "0.1.0-alpha.4", path = "crates/brk_fetcher" }
brk_indexer = { version = "0.1.0-alpha.4", path = "crates/brk_indexer" }
brk_iterator = { version = "0.1.0-alpha.4", path = "crates/brk_iterator" }
brk_logger = { version = "0.1.0-alpha.4", path = "crates/brk_logger" }
brk_mempool = { version = "0.1.0-alpha.4", path = "crates/brk_mempool" }
brk_query = { version = "0.1.0-alpha.4", path = "crates/brk_query", features = ["tokio"] }
brk_reader = { version = "0.1.0-alpha.4", path = "crates/brk_reader" }
brk_rpc = { version = "0.1.0-alpha.4", path = "crates/brk_rpc" }
brk_server = { version = "0.1.0-alpha.4", path = "crates/brk_server" }
brk_store = { version = "0.1.0-alpha.4", path = "crates/brk_store" }
brk_traversable = { version = "0.1.0-alpha.4", path = "crates/brk_traversable", features = ["pco", "derive"] }
brk_traversable_derive = { version = "0.1.0-alpha.4", path = "crates/brk_traversable_derive" }
brk_types = { version = "0.1.0-alpha.4", path = "crates/brk_types" }
byteview = "0.10.0"
color-eyre = "0.6.5"
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"] }
parking_lot = "0.12.5"
rayon = "1.11.0"
rustc-hash = "2.1.1"
schemars = "1.2.0"
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"] }
tracing = { version = "0.1", default-features = false, features = ["std"] }
vecdb = { version = "0.5.11", features = ["derive", "serde_json", "pco", "schemars"] }
# vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] }
[workspace.metadata.release]
shared-version = true
tag-name = "v{{version}}"
pre-release-commit-message = "release: v{{version}}"
tag-message = "release: v{{version}}"
[workspace.metadata.dist]
cargo-dist-version = "0.30.2"
ci = "github"
allow-dirty = ["ci"]
installers = []
targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-unknown-linux-gnu"]
-188
View File
@@ -1,188 +0,0 @@
<p align="center">
<a href="https://kibo.money" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/kibo-money/kibo/main/assets/logo-full-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/kibo-money/kibo/main/assets/logo-full-light.svg">
<img alt="kibō" src="https://raw.githubusercontent.com/kibo-money/kibo/main/assets/logo-full-light.svg" width="300" height="auto" style="max-width: 100%;">
</picture>
</a>
</p>
<p align="center">
<span>Bitcoin is our only <b><i>hope</i></b> for a better future.</span>
</p>
## Description
**kibō** (*hope* in japanese, formerly Satonomics) is a suite of tools that aims to help you understand Bitcoin's various dynamics. To do that, there is a wide number of charts and datasets with a scale by date but also by height free for you to explore. Which allows you to verify an incredible number of things, from the number of UTXOs to the repartition of the supply between different groups over time, with many things in between and it's all made possible thanks to Bitcoin's transparency. Whether you're an enthusiast, a researcher, a miner, an analyst, a trader, a skeptic or just curious, there is something new to learn for everyone !
While it's not the first tool trying to solve this problem, it's the first that is completely free, open-source and self-hostable. Which is very important as, just like for Bitcoin itself, I strongly believe that everyone should have access to this kind of data.
If you are a user of [mempool.space](https://mempool.space), you'll find this to be very complimentary, as it's a global and macro view of the chain over time instead.
If we want the world to move towards and, in the end, to be on a Bitcoin standard, we must have tools like this at our disposal.
## Donations
This project was started as an answer to the outrageous pricing from Glassnode (and their third tier starting at $833.33/month !).
But it is a lot of work and has been worked on **full-time since November of 2023** and has also been operational since then without any ads.
**At the time of writing (2024-09-12), this project has made around 2,200,000 sats, which is around $1300 or $120/month. While I'm very grateful for all donations, it's sadly unsustainable.**
So if you find this project useful, [please send some sats](https://geyser.fund/project/kibo/), it would be really appreciated.
If you're a potential sponsor, feel free to contact me in Nostr !
[Geyser Fund](https://geyser.fund/project/kibo/)
## Warning
This project is still in an early stage. Until more people look at the code and check the various computations in it, the datasets might be, in the worst case, completely false.
## Instances
- [kibo.money](https://kibo.money)
- [backup.kibo.money](https://backup.kibo.money)
## Structure
- `parser`: The backbone of the project, it does most of the work by parsing and then computing datasets from the timechain
- `website`: A web app which displays the generated datasets in various charts
- `server`: A small server which will serve both the website and the computed datasets via an API
## Roadmap
- **More Datasets/Charts**
- **Simulations**
- **Dashboards**
- **Nostr integration**
- **API Documentation**
- **Descriptions**
- **Start9 support**
## Setup
### Requirements
- At least 16 GB of RAM
- 1 TB of free space (will use 60-80% of that)
- A running instance of bitcoin-core with txindex=1 and rpc credentials
- Git
### Docker
Coming soon
### Manual
First we need to install Rust (https://www.rust-lang.org/tools/install)
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
If you already had Rust installed you could update it just in case
```bash
rustup update
```
Optionally, you can also install `cargo-watch` for the server to automatically restart it on file change, which will be triggered by new code and new datasets from the parser (https://github.com/watchexec/cargo-watch?tab=readme-ov-file#install)
```bash
cargo install cargo-watch --locked
```
Then you need to choose a path where all files related to **kibō** will live
```bash
cd ???
```
We can now clone the repository
```bash
git clone https://github.com/kibo-money/kibo.git
```
In a new terminal, go to the `parser`'s folder of the repository
```bash
cd ???/kibo/parser
```
Now we can finally start by running the parser, you need to use the `./run.sh` script instead of `cargo run -r` as we need to set various system variables for the program to run smoothly
For the first launch, the parser will need several information such as:
- `--datadir`: which is bitcoin data directory path
- `--rpcuser`: the username of the RPC credentials to talk to the bitcoin server
- `--rpcpassword`: the password of the RPC credentials
Everything will be saved in a `config.toml` file, which will allow you to simply run `./run.sh` next time
Here's an example
```bash
./run.sh --datadir=$HOME/Developer/bitcoin --rpcuser=satoshi --rpcpassword=nakamoto
```
In a new terminal, go to the `server`'s folder of the repository
```bash
cd ???/kibo/server
```
And start it also with the `run.sh` script instead of `cargo run -r`
```bash
./run.sh
```
Then the easiest to let others access your server is to use `cloudflared` which will also cache requests. For more information go to: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/
## Brand
### Name
kibō means _**hope**_ in japanese which is what Bitcoin ultimately is for many, hope for a better future.
### Logo
The dove (borrowed from [svgrepo](https://www.svgrepo.com/svg/351969/dove) for now) is known to represent hope in many cultures.
The orange background is a wink to Bitcoin and when in a circle, it also represents the sun, which means that while it's our hope for a better future, we still have to be careful with our collective goals and actions, to not end up like Icarus.
## Infrastructure
Here's the current infrastructure of the main instance and its backup.
It uses 2 servers, a full and a light one without the parser running but with still datasets syncronized via Syncthing.
Cloudflare is used for their tunnel + CDN services.
Though it's recommended to change to default **Browser Cache TTL** configuration from `4 Hours` to `Respect Existing Headers` (in `Websites / YOUR_DOMAIN / Caching / Configuration / Browser Cache TTL`) and activate `Always use https`.
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/kibo-money/kibo/main/assets/infrastructure-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/kibo-money/kibo/main/assets/infrastructure-light.svg">
<img alt="kibō" src="https://raw.githubusercontent.com/kibo-money/kibo/main/assets/infrastructure-light.svg" width="768" height="auto" style="max-width: 100%;">
</picture>
</p>
## Iterations
A list of all the previous versions and ideas:
- https://github.com/drgarlic/satonomics
- https://github.com/drgarlic/satonomics-parser
- https://github.com/drgarlic/satonomics-explorer
- https://github.com/drgarlic/satonomics-server
- https://github.com/drgarlic/satonomics-app
- https://github.com/drgarlic/bitalisys
- https://github.com/drgarlic/bitesque-app
- https://github.com/drgarlic/bitesque-back
- https://github.com/drgarlic/bitesque-front
- https://github.com/drgarlic/bitesque-assets
- https://github.com/drgarlic/syf
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because it is too large Load Diff
-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M 10.874 7.57 L 10.874 6.802 C 10.104 5.809 9.587 4.634 9.397 3.379 C 9.339 3.009 8.877 2.865 8.637 3.152 C 8.059 3.833 7.605 4.632 7.299 5.517 C 8.234 6.565 9.486 7.284 10.874 7.57 Z M 13.937 4.749 C 12.729 4.749 11.751 5.731 11.751 6.939 L 11.751 8.563 C 8.895 8.394 6.474 6.636 5.379 4.142 C 5.229 3.8 4.746 3.78 4.585 4.117 C 4.131 5.077 3.876 6.149 3.876 7.28 C 3.876 9.217 4.808 11.025 6.203 12.364 C 6.562 12.711 6.915 12.998 7.266 13.261 L 3.331 14.245 C 3.038 14.318 2.907 14.658 3.072 14.912 C 3.547 15.648 4.723 16.895 7.261 16.999 C 7.48 17.007 7.698 16.928 7.864 16.783 L 9.648 15.249 L 11.751 15.249 C 14.167 15.249 16.125 13.293 16.125 10.876 L 16.125 6.499 L 17 4.749 L 13.937 4.749 Z M 13.937 7.391 C 13.697 7.391 13.458 7.175 13.458 6.935 C 13.458 6.695 13.697 6.483 13.937 6.483 C 14.177 6.483 14.399 6.703 14.399 6.943 C 14.399 7.183 14.177 7.391 13.937 7.391 Z" style="fill: #12100f;"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M 10.874 7.57 L 10.874 6.802 C 10.104 5.809 9.587 4.634 9.397 3.379 C 9.339 3.009 8.877 2.865 8.637 3.152 C 8.059 3.833 7.605 4.632 7.299 5.517 C 8.234 6.565 9.486 7.284 10.874 7.57 Z M 13.937 4.749 C 12.729 4.749 11.751 5.731 11.751 6.939 L 11.751 8.563 C 8.895 8.394 6.474 6.636 5.379 4.142 C 5.229 3.8 4.746 3.78 4.585 4.117 C 4.131 5.077 3.876 6.149 3.876 7.28 C 3.876 9.217 4.808 11.025 6.203 12.364 C 6.562 12.711 6.915 12.998 7.266 13.261 L 3.331 14.245 C 3.038 14.318 2.907 14.658 3.072 14.912 C 3.547 15.648 4.723 16.895 7.261 16.999 C 7.48 17.007 7.698 16.928 7.864 16.783 L 9.648 15.249 L 11.751 15.249 C 14.167 15.249 16.125 13.293 16.125 10.876 L 16.125 6.499 L 17 4.749 L 13.937 4.749 Z M 13.937 7.391 C 13.697 7.391 13.458 7.175 13.458 6.935 C 13.458 6.695 13.697 6.483 13.937 6.483 C 14.177 6.483 14.399 6.703 14.399 6.943 C 14.399 7.183 14.177 7.391 13.937 7.391 Z" style="fill: #fffaf6;"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M 10.874 7.57 L 10.874 6.802 C 10.104 5.809 9.587 4.634 9.397 3.379 C 9.339 3.009 8.877 2.865 8.637 3.152 C 8.059 3.833 7.605 4.632 7.299 5.517 C 8.234 6.565 9.486 7.284 10.874 7.57 Z M 13.937 4.749 C 12.729 4.749 11.751 5.731 11.751 6.939 L 11.751 8.563 C 8.895 8.394 6.474 6.636 5.379 4.142 C 5.229 3.8 4.746 3.78 4.585 4.117 C 4.131 5.077 3.876 6.149 3.876 7.28 C 3.876 9.217 4.808 11.025 6.203 12.364 C 6.562 12.711 6.915 12.998 7.266 13.261 L 3.331 14.245 C 3.038 14.318 2.907 14.658 3.072 14.912 C 3.547 15.648 4.723 16.895 7.261 16.999 C 7.48 17.007 7.698 16.928 7.864 16.783 L 9.648 15.249 L 11.751 15.249 C 14.167 15.249 16.125 13.293 16.125 10.876 L 16.125 6.499 L 17 4.749 L 13.937 4.749 Z M 13.937 7.391 C 13.697 7.391 13.458 7.175 13.458 6.935 C 13.458 6.695 13.697 6.483 13.937 6.483 C 14.177 6.483 14.399 6.703 14.399 6.943 C 14.399 7.183 14.177 7.391 13.937 7.391 Z" style="fill: #f26610;"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

-11
View File
@@ -1,11 +0,0 @@
<svg viewBox="0 0 720 180" xmlns="http://www.w3.org/2000/svg">
<defs></defs>
<g transform="matrix(7.5, 0, 0, 7.5, -2046.71228, -1592.744873)">
<ellipse style="fill: #f26610;" cx="284.895" cy="224.366" rx="12" ry="12"></ellipse>
<path d="M 285.769 221.936 L 285.769 221.168 C 284.999 220.175 284.482 219 284.292 217.745 C 284.234 217.375 283.772 217.231 283.532 217.518 C 282.954 218.199 282.5 218.998 282.194 219.883 C 283.129 220.931 284.381 221.65 285.769 221.936 Z M 288.832 219.115 C 287.624 219.115 286.646 220.097 286.646 221.305 L 286.646 222.929 C 283.79 222.76 281.369 221.002 280.274 218.508 C 280.124 218.166 279.641 218.146 279.48 218.483 C 279.026 219.443 278.771 220.515 278.771 221.646 C 278.771 223.583 279.703 225.391 281.098 226.73 C 281.457 227.077 281.81 227.364 282.161 227.627 L 278.226 228.611 C 277.933 228.684 277.802 229.024 277.967 229.278 C 278.442 230.014 279.618 231.261 282.156 231.365 C 282.375 231.373 282.593 231.294 282.759 231.149 L 284.543 229.615 L 286.646 229.615 C 289.062 229.615 291.02 227.659 291.02 225.242 L 291.02 220.865 L 291.895 219.115 L 288.832 219.115 Z M 288.832 221.757 C 288.592 221.757 288.353 221.541 288.353 221.301 C 288.353 221.061 288.592 220.849 288.832 220.849 C 289.072 220.849 289.294 221.069 289.294 221.309 C 289.294 221.549 289.072 221.757 288.832 221.757 Z" style="fill: #fffaf6;"></path>
</g>
<g transform="matrix(1, 0, 0, 1, -30, 0)">
<path d="M 278.049 146.789 L 278.049 127.527 L 287.141 117.972 L 304.4 146.789 L 331.83 146.789 L 303.784 100.251 L 332.755 69.739 L 303.013 69.739 L 278.049 97.477 L 278.049 30.598 L 254.318 30.598 L 254.318 146.789 L 278.049 146.789 Z M 354.169 57.719 C 361.565 57.719 367.575 51.709 367.575 44.158 C 367.575 36.608 361.565 30.752 354.169 30.752 C 346.618 30.752 340.608 36.608 340.608 44.158 C 340.608 51.709 346.618 57.719 354.169 57.719 Z M 342.457 146.789 L 366.188 146.789 L 366.188 69.739 L 342.457 69.739 L 342.457 146.789 Z M 406.407 146.789 L 407.64 136.927 C 411.801 144.015 421.047 148.792 431.834 148.792 C 453.716 148.792 468.972 132.92 468.972 109.035 C 468.972 83.916 455.257 67.119 433.683 67.119 C 422.588 67.119 412.417 71.742 407.794 78.677 L 407.794 30.598 L 384.063 30.598 L 384.063 146.789 L 406.407 146.789 Z M 407.948 107.802 C 407.948 96.244 415.653 88.539 426.749 88.539 C 437.998 88.539 445.087 96.398 445.087 107.802 C 445.087 119.205 437.998 127.064 426.749 127.064 C 415.653 127.064 407.948 119.359 407.948 107.802 Z M 498.713 56.332 L 543.402 56.332 L 543.402 40.306 L 498.713 40.306 L 498.713 56.332 Z M 478.526 108.11 C 478.526 132.458 496.402 148.638 521.058 148.638 C 545.56 148.638 563.435 132.458 563.435 108.11 C 563.435 83.762 545.56 67.428 521.058 67.428 C 496.402 67.428 478.526 83.762 478.526 108.11 Z M 502.412 107.956 C 502.412 96.398 509.963 88.693 521.058 88.693 C 531.999 88.693 539.55 96.398 539.55 107.956 C 539.55 119.667 531.999 127.372 521.058 127.372 C 509.963 127.372 502.412 119.667 502.412 107.956 Z" style="fill: #fffaf6;"></path>
<path d="M 589.19 97.802 L 589.19 106.23 L 610.948 106.23 C 605.1 112.938 597.446 119.044 587.986 124.376 L 593.404 131.514 C 597.532 128.934 601.488 126.268 605.186 123.43 L 605.186 146.048 L 614.13 146.048 L 614.13 123.43 L 626.944 123.43 L 626.944 149.402 L 635.974 149.402 L 635.974 123.43 L 649.82 123.43 L 649.82 134.008 C 649.82 136.072 649.046 137.104 647.498 137.104 L 640.36 136.674 L 642.768 145.188 L 650.422 145.188 C 655.926 145.188 658.678 142.092 658.678 135.986 L 658.678 115.174 L 635.974 115.174 L 635.974 108.638 L 626.944 108.638 L 626.944 115.174 L 614.388 115.174 C 617.054 112.336 619.548 109.326 621.784 106.23 L 665.128 106.23 L 665.128 97.802 L 626.858 97.802 C 627.89 95.824 628.836 93.76 629.696 91.61 L 620.838 90.492 C 619.806 92.9 618.516 95.394 617.14 97.802 L 589.19 97.802 Z M 648.1 68.734 C 642.338 72.088 636.232 75.098 629.868 77.678 C 621.612 75.012 612.926 72.518 603.896 70.282 L 599.252 77.248 C 605.272 78.624 611.206 80.258 617.226 82.15 C 610.088 84.386 602.606 86.106 594.78 87.482 L 599.596 95.308 C 612.324 92.04 622.472 89.116 630.04 86.364 C 638.124 89.116 646.122 92.298 654.034 95.824 L 658.936 88.428 C 653.26 86.02 647.412 83.698 641.392 81.548 C 646.208 79.226 651.11 76.56 655.926 73.55 L 648.1 68.734 Z M 675.438 77.85 L 675.438 85.848 L 682.404 85.848 L 682.404 98.92 C 682.404 101.5 681.114 103.22 678.62 104.166 L 680.684 110.874 C 692.036 108.896 701.926 106.66 710.182 104.08 L 708.634 96.426 C 703.474 98.146 697.454 99.608 690.574 100.984 L 690.574 85.848 L 712.332 85.848 L 712.332 77.85 L 698.916 77.85 C 698.4 74.668 697.884 71.744 697.368 69.164 L 688.338 70.712 C 688.94 72.862 689.542 75.27 690.144 77.85 L 675.438 77.85 Z M 724.028 89.632 L 739.25 89.632 L 739.25 93.502 L 723.856 93.502 C 723.942 92.47 724.028 91.352 724.028 90.32 L 724.028 89.632 Z M 739.25 83.096 L 724.028 83.096 L 724.028 79.226 L 739.25 79.226 L 739.25 83.096 Z M 722.652 100.038 L 739.25 100.038 L 739.25 100.898 C 739.25 103.048 738.218 104.166 736.24 104.166 C 733.918 104.166 731.424 103.994 728.758 103.822 L 730.822 111.562 L 738.734 111.562 C 744.582 111.562 747.506 108.982 747.506 103.908 L 747.506 72.002 L 715.6 72.002 L 715.6 90.922 C 715.428 97.286 713.192 102.532 708.892 106.746 L 715.342 112.594 C 718.782 109.068 721.276 104.854 722.652 100.038 Z M 708.462 121.452 L 708.462 126.784 L 683.608 126.784 L 683.608 134.352 L 708.462 134.352 L 708.462 139.598 L 675.524 139.598 L 675.524 147.51 L 750 147.51 L 750 139.598 L 717.062 139.598 L 717.062 134.352 L 742.174 134.352 L 742.174 126.784 L 717.062 126.784 L 717.062 121.452 L 746.216 121.452 L 746.216 113.712 L 679.308 113.712 L 679.308 121.452 L 708.462 121.452 Z" style="fill: #68625f"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.6 KiB

-11
View File
@@ -1,11 +0,0 @@
<svg viewBox="0 0 720 180" xmlns="http://www.w3.org/2000/svg">
<defs></defs>
<g transform="matrix(7.5, 0, 0, 7.5, -2046.71228, -1592.744873)">
<ellipse style="fill: #f26610;" cx="284.895" cy="224.366" rx="12" ry="12"></ellipse>
<path d="M 285.769 221.936 L 285.769 221.168 C 284.999 220.175 284.482 219 284.292 217.745 C 284.234 217.375 283.772 217.231 283.532 217.518 C 282.954 218.199 282.5 218.998 282.194 219.883 C 283.129 220.931 284.381 221.65 285.769 221.936 Z M 288.832 219.115 C 287.624 219.115 286.646 220.097 286.646 221.305 L 286.646 222.929 C 283.79 222.76 281.369 221.002 280.274 218.508 C 280.124 218.166 279.641 218.146 279.48 218.483 C 279.026 219.443 278.771 220.515 278.771 221.646 C 278.771 223.583 279.703 225.391 281.098 226.73 C 281.457 227.077 281.81 227.364 282.161 227.627 L 278.226 228.611 C 277.933 228.684 277.802 229.024 277.967 229.278 C 278.442 230.014 279.618 231.261 282.156 231.365 C 282.375 231.373 282.593 231.294 282.759 231.149 L 284.543 229.615 L 286.646 229.615 C 289.062 229.615 291.02 227.659 291.02 225.242 L 291.02 220.865 L 291.895 219.115 L 288.832 219.115 Z M 288.832 221.757 C 288.592 221.757 288.353 221.541 288.353 221.301 C 288.353 221.061 288.592 220.849 288.832 220.849 C 289.072 220.849 289.294 221.069 289.294 221.309 C 289.294 221.549 289.072 221.757 288.832 221.757 Z" style="fill: #fffaf6;"></path>
</g>
<g transform="matrix(1, 0, 0, 1, -30, 0)">
<path d="M 278.049 146.789 L 278.049 127.527 L 287.141 117.972 L 304.4 146.789 L 331.83 146.789 L 303.784 100.251 L 332.755 69.739 L 303.013 69.739 L 278.049 97.477 L 278.049 30.598 L 254.318 30.598 L 254.318 146.789 L 278.049 146.789 Z M 354.169 57.719 C 361.565 57.719 367.575 51.709 367.575 44.158 C 367.575 36.608 361.565 30.752 354.169 30.752 C 346.618 30.752 340.608 36.608 340.608 44.158 C 340.608 51.709 346.618 57.719 354.169 57.719 Z M 342.457 146.789 L 366.188 146.789 L 366.188 69.739 L 342.457 69.739 L 342.457 146.789 Z M 406.407 146.789 L 407.64 136.927 C 411.801 144.015 421.047 148.792 431.834 148.792 C 453.716 148.792 468.972 132.92 468.972 109.035 C 468.972 83.916 455.257 67.119 433.683 67.119 C 422.588 67.119 412.417 71.742 407.794 78.677 L 407.794 30.598 L 384.063 30.598 L 384.063 146.789 L 406.407 146.789 Z M 407.948 107.802 C 407.948 96.244 415.653 88.539 426.749 88.539 C 437.998 88.539 445.087 96.398 445.087 107.802 C 445.087 119.205 437.998 127.064 426.749 127.064 C 415.653 127.064 407.948 119.359 407.948 107.802 Z M 498.713 56.332 L 543.402 56.332 L 543.402 40.306 L 498.713 40.306 L 498.713 56.332 Z M 478.526 108.11 C 478.526 132.458 496.402 148.638 521.058 148.638 C 545.56 148.638 563.435 132.458 563.435 108.11 C 563.435 83.762 545.56 67.428 521.058 67.428 C 496.402 67.428 478.526 83.762 478.526 108.11 Z M 502.412 107.956 C 502.412 96.398 509.963 88.693 521.058 88.693 C 531.999 88.693 539.55 96.398 539.55 107.956 C 539.55 119.667 531.999 127.372 521.058 127.372 C 509.963 127.372 502.412 119.667 502.412 107.956 Z" style="fill: #12100f;"></path>
<path d="M 589.19 97.802 L 589.19 106.23 L 610.948 106.23 C 605.1 112.938 597.446 119.044 587.986 124.376 L 593.404 131.514 C 597.532 128.934 601.488 126.268 605.186 123.43 L 605.186 146.048 L 614.13 146.048 L 614.13 123.43 L 626.944 123.43 L 626.944 149.402 L 635.974 149.402 L 635.974 123.43 L 649.82 123.43 L 649.82 134.008 C 649.82 136.072 649.046 137.104 647.498 137.104 L 640.36 136.674 L 642.768 145.188 L 650.422 145.188 C 655.926 145.188 658.678 142.092 658.678 135.986 L 658.678 115.174 L 635.974 115.174 L 635.974 108.638 L 626.944 108.638 L 626.944 115.174 L 614.388 115.174 C 617.054 112.336 619.548 109.326 621.784 106.23 L 665.128 106.23 L 665.128 97.802 L 626.858 97.802 C 627.89 95.824 628.836 93.76 629.696 91.61 L 620.838 90.492 C 619.806 92.9 618.516 95.394 617.14 97.802 L 589.19 97.802 Z M 648.1 68.734 C 642.338 72.088 636.232 75.098 629.868 77.678 C 621.612 75.012 612.926 72.518 603.896 70.282 L 599.252 77.248 C 605.272 78.624 611.206 80.258 617.226 82.15 C 610.088 84.386 602.606 86.106 594.78 87.482 L 599.596 95.308 C 612.324 92.04 622.472 89.116 630.04 86.364 C 638.124 89.116 646.122 92.298 654.034 95.824 L 658.936 88.428 C 653.26 86.02 647.412 83.698 641.392 81.548 C 646.208 79.226 651.11 76.56 655.926 73.55 L 648.1 68.734 Z M 675.438 77.85 L 675.438 85.848 L 682.404 85.848 L 682.404 98.92 C 682.404 101.5 681.114 103.22 678.62 104.166 L 680.684 110.874 C 692.036 108.896 701.926 106.66 710.182 104.08 L 708.634 96.426 C 703.474 98.146 697.454 99.608 690.574 100.984 L 690.574 85.848 L 712.332 85.848 L 712.332 77.85 L 698.916 77.85 C 698.4 74.668 697.884 71.744 697.368 69.164 L 688.338 70.712 C 688.94 72.862 689.542 75.27 690.144 77.85 L 675.438 77.85 Z M 724.028 89.632 L 739.25 89.632 L 739.25 93.502 L 723.856 93.502 C 723.942 92.47 724.028 91.352 724.028 90.32 L 724.028 89.632 Z M 739.25 83.096 L 724.028 83.096 L 724.028 79.226 L 739.25 79.226 L 739.25 83.096 Z M 722.652 100.038 L 739.25 100.038 L 739.25 100.898 C 739.25 103.048 738.218 104.166 736.24 104.166 C 733.918 104.166 731.424 103.994 728.758 103.822 L 730.822 111.562 L 738.734 111.562 C 744.582 111.562 747.506 108.982 747.506 103.908 L 747.506 72.002 L 715.6 72.002 L 715.6 90.922 C 715.428 97.286 713.192 102.532 708.892 106.746 L 715.342 112.594 C 718.782 109.068 721.276 104.854 722.652 100.038 Z M 708.462 121.452 L 708.462 126.784 L 683.608 126.784 L 683.608 134.352 L 708.462 134.352 L 708.462 139.598 L 675.524 139.598 L 675.524 147.51 L 750 147.51 L 750 139.598 L 717.062 139.598 L 717.062 134.352 L 742.174 134.352 L 742.174 126.784 L 717.062 126.784 L 717.062 121.452 L 746.216 121.452 L 746.216 113.712 L 679.308 113.712 L 679.308 121.452 L 708.462 121.452 Z" style="fill: #b4aca9;"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.6 KiB

-5
View File
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<ellipse style="fill: #f26610;" cx="12" cy="12" rx="12" ry="12"/>
<path d="M 12.874 9.57 L 12.874 8.802 C 12.104 7.809 11.587 6.634 11.397 5.379 C 11.339 5.009 10.877 4.865 10.637 5.152 C 10.059 5.833 9.605 6.632 9.299 7.517 C 10.234 8.565 11.486 9.284 12.874 9.57 Z M 15.937 6.749 C 14.729 6.749 13.751 7.731 13.751 8.939 L 13.751 10.563 C 10.895 10.394 8.474 8.636 7.379 6.142 C 7.229 5.8 6.746 5.78 6.585 6.117 C 6.131 7.077 5.876 8.149 5.876 9.28 C 5.876 11.217 6.808 13.025 8.203 14.364 C 8.562 14.711 8.915 14.998 9.266 15.261 L 5.331 16.245 C 5.038 16.318 4.907 16.658 5.072 16.912 C 5.547 17.648 6.723 18.895 9.261 18.999 C 9.48 19.007 9.698 18.928 9.864 18.783 L 11.648 17.249 L 13.751 17.249 C 16.167 17.249 18.125 15.293 18.125 12.876 L 18.125 8.499 L 19 6.749 L 15.937 6.749 Z M 15.937 9.391 C 15.697 9.391 15.458 9.175 15.458 8.935 C 15.458 8.695 15.697 8.483 15.937 8.483 C 16.177 8.483 16.399 8.703 16.399 8.943 C 16.399 9.183 16.177 9.391 15.937 9.391 Z" style="fill: #fffaf6;"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 500 180" xmlns="http://www.w3.org/2000/svg">
<defs/>
<g transform="matrix(1, 0, 0, 1, -252.158997, 0)">
<path d="M 278.049 146.789 L 278.049 127.527 L 287.141 117.972 L 304.4 146.789 L 331.83 146.789 L 303.784 100.251 L 332.755 69.739 L 303.013 69.739 L 278.049 97.477 L 278.049 30.598 L 254.318 30.598 L 254.318 146.789 L 278.049 146.789 Z M 354.169 57.719 C 361.565 57.719 367.575 51.709 367.575 44.158 C 367.575 36.608 361.565 30.752 354.169 30.752 C 346.618 30.752 340.608 36.608 340.608 44.158 C 340.608 51.709 346.618 57.719 354.169 57.719 Z M 342.457 146.789 L 366.188 146.789 L 366.188 69.739 L 342.457 69.739 L 342.457 146.789 Z M 406.407 146.789 L 407.64 136.927 C 411.801 144.015 421.047 148.792 431.834 148.792 C 453.716 148.792 468.972 132.92 468.972 109.035 C 468.972 83.916 455.257 67.119 433.683 67.119 C 422.588 67.119 412.417 71.742 407.794 78.677 L 407.794 30.598 L 384.063 30.598 L 384.063 146.789 L 406.407 146.789 Z M 407.948 107.802 C 407.948 96.244 415.653 88.539 426.749 88.539 C 437.998 88.539 445.087 96.398 445.087 107.802 C 445.087 119.205 437.998 127.064 426.749 127.064 C 415.653 127.064 407.948 119.359 407.948 107.802 Z M 498.713 56.332 L 543.402 56.332 L 543.402 40.306 L 498.713 40.306 L 498.713 56.332 Z M 478.526 108.11 C 478.526 132.458 496.402 148.638 521.058 148.638 C 545.56 148.638 563.435 132.458 563.435 108.11 C 563.435 83.762 545.56 67.428 521.058 67.428 C 496.402 67.428 478.526 83.762 478.526 108.11 Z M 502.412 107.956 C 502.412 96.398 509.963 88.693 521.058 88.693 C 531.999 88.693 539.55 96.398 539.55 107.956 C 539.55 119.667 531.999 127.372 521.058 127.372 C 509.963 127.372 502.412 119.667 502.412 107.956 Z" style="fill: rgb(16, 16, 14);"/>
<path d="M 589.19 97.802 L 589.19 106.23 L 610.948 106.23 C 605.1 112.938 597.446 119.044 587.986 124.376 L 593.404 131.514 C 597.532 128.934 601.488 126.268 605.186 123.43 L 605.186 146.048 L 614.13 146.048 L 614.13 123.43 L 626.944 123.43 L 626.944 149.402 L 635.974 149.402 L 635.974 123.43 L 649.82 123.43 L 649.82 134.008 C 649.82 136.072 649.046 137.104 647.498 137.104 L 640.36 136.674 L 642.768 145.188 L 650.422 145.188 C 655.926 145.188 658.678 142.092 658.678 135.986 L 658.678 115.174 L 635.974 115.174 L 635.974 108.638 L 626.944 108.638 L 626.944 115.174 L 614.388 115.174 C 617.054 112.336 619.548 109.326 621.784 106.23 L 665.128 106.23 L 665.128 97.802 L 626.858 97.802 C 627.89 95.824 628.836 93.76 629.696 91.61 L 620.838 90.492 C 619.806 92.9 618.516 95.394 617.14 97.802 L 589.19 97.802 Z M 648.1 68.734 C 642.338 72.088 636.232 75.098 629.868 77.678 C 621.612 75.012 612.926 72.518 603.896 70.282 L 599.252 77.248 C 605.272 78.624 611.206 80.258 617.226 82.15 C 610.088 84.386 602.606 86.106 594.78 87.482 L 599.596 95.308 C 612.324 92.04 622.472 89.116 630.04 86.364 C 638.124 89.116 646.122 92.298 654.034 95.824 L 658.936 88.428 C 653.26 86.02 647.412 83.698 641.392 81.548 C 646.208 79.226 651.11 76.56 655.926 73.55 L 648.1 68.734 Z M 675.438 77.85 L 675.438 85.848 L 682.404 85.848 L 682.404 98.92 C 682.404 101.5 681.114 103.22 678.62 104.166 L 680.684 110.874 C 692.036 108.896 701.926 106.66 710.182 104.08 L 708.634 96.426 C 703.474 98.146 697.454 99.608 690.574 100.984 L 690.574 85.848 L 712.332 85.848 L 712.332 77.85 L 698.916 77.85 C 698.4 74.668 697.884 71.744 697.368 69.164 L 688.338 70.712 C 688.94 72.862 689.542 75.27 690.144 77.85 L 675.438 77.85 Z M 724.028 89.632 L 739.25 89.632 L 739.25 93.502 L 723.856 93.502 C 723.942 92.47 724.028 91.352 724.028 90.32 L 724.028 89.632 Z M 739.25 83.096 L 724.028 83.096 L 724.028 79.226 L 739.25 79.226 L 739.25 83.096 Z M 722.652 100.038 L 739.25 100.038 L 739.25 100.898 C 739.25 103.048 738.218 104.166 736.24 104.166 C 733.918 104.166 731.424 103.994 728.758 103.822 L 730.822 111.562 L 738.734 111.562 C 744.582 111.562 747.506 108.982 747.506 103.908 L 747.506 72.002 L 715.6 72.002 L 715.6 90.922 C 715.428 97.286 713.192 102.532 708.892 106.746 L 715.342 112.594 C 718.782 109.068 721.276 104.854 722.652 100.038 Z M 708.462 121.452 L 708.462 126.784 L 683.608 126.784 L 683.608 134.352 L 708.462 134.352 L 708.462 139.598 L 675.524 139.598 L 675.524 147.51 L 750 147.51 L 750 139.598 L 717.062 139.598 L 717.062 134.352 L 742.174 134.352 L 742.174 126.784 L 717.062 126.784 L 717.062 121.452 L 746.216 121.452 L 746.216 113.712 L 679.308 113.712 L 679.308 121.452 L 708.462 121.452 Z" style="fill: rgb(192, 192, 171);"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 500 180" xmlns="http://www.w3.org/2000/svg">
<defs/>
<g transform="matrix(1, 0, 0, 1, -252.158997, 0)">
<path d="M 278.049 146.789 L 278.049 127.527 L 287.141 117.972 L 304.4 146.789 L 331.83 146.789 L 303.784 100.251 L 332.755 69.739 L 303.013 69.739 L 278.049 97.477 L 278.049 30.598 L 254.318 30.598 L 254.318 146.789 L 278.049 146.789 Z M 354.169 57.719 C 361.565 57.719 367.575 51.709 367.575 44.158 C 367.575 36.608 361.565 30.752 354.169 30.752 C 346.618 30.752 340.608 36.608 340.608 44.158 C 340.608 51.709 346.618 57.719 354.169 57.719 Z M 342.457 146.789 L 366.188 146.789 L 366.188 69.739 L 342.457 69.739 L 342.457 146.789 Z M 406.407 146.789 L 407.64 136.927 C 411.801 144.015 421.047 148.792 431.834 148.792 C 453.716 148.792 468.972 132.92 468.972 109.035 C 468.972 83.916 455.257 67.119 433.683 67.119 C 422.588 67.119 412.417 71.742 407.794 78.677 L 407.794 30.598 L 384.063 30.598 L 384.063 146.789 L 406.407 146.789 Z M 407.948 107.802 C 407.948 96.244 415.653 88.539 426.749 88.539 C 437.998 88.539 445.087 96.398 445.087 107.802 C 445.087 119.205 437.998 127.064 426.749 127.064 C 415.653 127.064 407.948 119.359 407.948 107.802 Z M 498.713 56.332 L 543.402 56.332 L 543.402 40.306 L 498.713 40.306 L 498.713 56.332 Z M 478.526 108.11 C 478.526 132.458 496.402 148.638 521.058 148.638 C 545.56 148.638 563.435 132.458 563.435 108.11 C 563.435 83.762 545.56 67.428 521.058 67.428 C 496.402 67.428 478.526 83.762 478.526 108.11 Z M 502.412 107.956 C 502.412 96.398 509.963 88.693 521.058 88.693 C 531.999 88.693 539.55 96.398 539.55 107.956 C 539.55 119.667 531.999 127.372 521.058 127.372 C 509.963 127.372 502.412 119.667 502.412 107.956 Z" style="fill: rgb(16, 16, 14);"/>
<path d="M 589.19 97.802 L 589.19 106.23 L 610.948 106.23 C 605.1 112.938 597.446 119.044 587.986 124.376 L 593.404 131.514 C 597.532 128.934 601.488 126.268 605.186 123.43 L 605.186 146.048 L 614.13 146.048 L 614.13 123.43 L 626.944 123.43 L 626.944 149.402 L 635.974 149.402 L 635.974 123.43 L 649.82 123.43 L 649.82 134.008 C 649.82 136.072 649.046 137.104 647.498 137.104 L 640.36 136.674 L 642.768 145.188 L 650.422 145.188 C 655.926 145.188 658.678 142.092 658.678 135.986 L 658.678 115.174 L 635.974 115.174 L 635.974 108.638 L 626.944 108.638 L 626.944 115.174 L 614.388 115.174 C 617.054 112.336 619.548 109.326 621.784 106.23 L 665.128 106.23 L 665.128 97.802 L 626.858 97.802 C 627.89 95.824 628.836 93.76 629.696 91.61 L 620.838 90.492 C 619.806 92.9 618.516 95.394 617.14 97.802 L 589.19 97.802 Z M 648.1 68.734 C 642.338 72.088 636.232 75.098 629.868 77.678 C 621.612 75.012 612.926 72.518 603.896 70.282 L 599.252 77.248 C 605.272 78.624 611.206 80.258 617.226 82.15 C 610.088 84.386 602.606 86.106 594.78 87.482 L 599.596 95.308 C 612.324 92.04 622.472 89.116 630.04 86.364 C 638.124 89.116 646.122 92.298 654.034 95.824 L 658.936 88.428 C 653.26 86.02 647.412 83.698 641.392 81.548 C 646.208 79.226 651.11 76.56 655.926 73.55 L 648.1 68.734 Z M 675.438 77.85 L 675.438 85.848 L 682.404 85.848 L 682.404 98.92 C 682.404 101.5 681.114 103.22 678.62 104.166 L 680.684 110.874 C 692.036 108.896 701.926 106.66 710.182 104.08 L 708.634 96.426 C 703.474 98.146 697.454 99.608 690.574 100.984 L 690.574 85.848 L 712.332 85.848 L 712.332 77.85 L 698.916 77.85 C 698.4 74.668 697.884 71.744 697.368 69.164 L 688.338 70.712 C 688.94 72.862 689.542 75.27 690.144 77.85 L 675.438 77.85 Z M 724.028 89.632 L 739.25 89.632 L 739.25 93.502 L 723.856 93.502 C 723.942 92.47 724.028 91.352 724.028 90.32 L 724.028 89.632 Z M 739.25 83.096 L 724.028 83.096 L 724.028 79.226 L 739.25 79.226 L 739.25 83.096 Z M 722.652 100.038 L 739.25 100.038 L 739.25 100.898 C 739.25 103.048 738.218 104.166 736.24 104.166 C 733.918 104.166 731.424 103.994 728.758 103.822 L 730.822 111.562 L 738.734 111.562 C 744.582 111.562 747.506 108.982 747.506 103.908 L 747.506 72.002 L 715.6 72.002 L 715.6 90.922 C 715.428 97.286 713.192 102.532 708.892 106.746 L 715.342 112.594 C 718.782 109.068 721.276 104.854 722.652 100.038 Z M 708.462 121.452 L 708.462 126.784 L 683.608 126.784 L 683.608 134.352 L 708.462 134.352 L 708.462 139.598 L 675.524 139.598 L 675.524 147.51 L 750 147.51 L 750 139.598 L 717.062 139.598 L 717.062 134.352 L 742.174 134.352 L 742.174 126.784 L 717.062 126.784 L 717.062 121.452 L 746.216 121.452 L 746.216 113.712 L 679.308 113.712 L 679.308 121.452 L 708.462 121.452 Z" style="fill: rgb(192, 192, 171);"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

-7
View File
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 310 180" xmlns="http://www.w3.org/2000/svg">
<defs/>
<g transform="matrix(1, 0, 0, 1, -253.876495, 0)">
<path d="M 278.049 146.789 L 278.049 127.527 L 287.141 117.972 L 304.4 146.789 L 331.83 146.789 L 303.784 100.251 L 332.755 69.739 L 303.013 69.739 L 278.049 97.477 L 278.049 30.598 L 254.318 30.598 L 254.318 146.789 L 278.049 146.789 Z M 354.169 57.719 C 361.565 57.719 367.575 51.709 367.575 44.158 C 367.575 36.608 361.565 30.752 354.169 30.752 C 346.618 30.752 340.608 36.608 340.608 44.158 C 340.608 51.709 346.618 57.719 354.169 57.719 Z M 342.457 146.789 L 366.188 146.789 L 366.188 69.739 L 342.457 69.739 L 342.457 146.789 Z M 406.407 146.789 L 407.64 136.927 C 411.801 144.015 421.047 148.792 431.834 148.792 C 453.716 148.792 468.972 132.92 468.972 109.035 C 468.972 83.916 455.257 67.119 433.683 67.119 C 422.588 67.119 412.417 71.742 407.794 78.677 L 407.794 30.598 L 384.063 30.598 L 384.063 146.789 L 406.407 146.789 Z M 407.948 107.802 C 407.948 96.244 415.653 88.539 426.749 88.539 C 437.998 88.539 445.087 96.398 445.087 107.802 C 445.087 119.205 437.998 127.064 426.749 127.064 C 415.653 127.064 407.948 119.359 407.948 107.802 Z M 498.713 56.332 L 543.402 56.332 L 543.402 40.306 L 498.713 40.306 L 498.713 56.332 Z M 478.526 108.11 C 478.526 132.458 496.402 148.638 521.058 148.638 C 545.56 148.638 563.435 132.458 563.435 108.11 C 563.435 83.762 545.56 67.428 521.058 67.428 C 496.402 67.428 478.526 83.762 478.526 108.11 Z M 502.412 107.956 C 502.412 96.398 509.963 88.693 521.058 88.693 C 531.999 88.693 539.55 96.398 539.55 107.956 C 539.55 119.667 531.999 127.372 521.058 127.372 C 509.963 127.372 502.412 119.667 502.412 107.956 Z" style="fill: rgb(16, 16, 14);"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

-7
View File
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 310 180" xmlns="http://www.w3.org/2000/svg">
<defs/>
<g transform="matrix(1, 0, 0, 1, -253.876495, 0)">
<path d="M 278.049 146.789 L 278.049 127.527 L 287.141 117.972 L 304.4 146.789 L 331.83 146.789 L 303.784 100.251 L 332.755 69.739 L 303.013 69.739 L 278.049 97.477 L 278.049 30.598 L 254.318 30.598 L 254.318 146.789 L 278.049 146.789 Z M 354.169 57.719 C 361.565 57.719 367.575 51.709 367.575 44.158 C 367.575 36.608 361.565 30.752 354.169 30.752 C 346.618 30.752 340.608 36.608 340.608 44.158 C 340.608 51.709 346.618 57.719 354.169 57.719 Z M 342.457 146.789 L 366.188 146.789 L 366.188 69.739 L 342.457 69.739 L 342.457 146.789 Z M 406.407 146.789 L 407.64 136.927 C 411.801 144.015 421.047 148.792 431.834 148.792 C 453.716 148.792 468.972 132.92 468.972 109.035 C 468.972 83.916 455.257 67.119 433.683 67.119 C 422.588 67.119 412.417 71.742 407.794 78.677 L 407.794 30.598 L 384.063 30.598 L 384.063 146.789 L 406.407 146.789 Z M 407.948 107.802 C 407.948 96.244 415.653 88.539 426.749 88.539 C 437.998 88.539 445.087 96.398 445.087 107.802 C 445.087 119.205 437.998 127.064 426.749 127.064 C 415.653 127.064 407.948 119.359 407.948 107.802 Z M 498.713 56.332 L 543.402 56.332 L 543.402 40.306 L 498.713 40.306 L 498.713 56.332 Z M 478.526 108.11 C 478.526 132.458 496.402 148.638 521.058 148.638 C 545.56 148.638 563.435 132.458 563.435 108.11 C 563.435 83.762 545.56 67.428 521.058 67.428 C 496.402 67.428 478.526 83.762 478.526 108.11 Z M 502.412 107.956 C 502.412 96.398 509.963 88.693 521.058 88.693 C 531.999 88.693 539.55 96.398 539.55 107.956 C 539.55 119.667 531.999 127.372 521.058 127.372 C 509.963 127.372 502.412 119.667 502.412 107.956 Z" style="fill: rgb(16, 16, 14);"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 496 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 564 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 592 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 453 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 526 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

+71
View File
@@ -0,0 +1,71 @@
[package]
name = "brk"
description.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
edition.workspace = true
version.workspace = true
[features]
full = [
"bencher",
"bindgen",
"client",
"computer",
"error",
"fetcher",
"cohort",
"indexer",
"iterator",
"logger",
"mempool",
"query",
"reader",
"rpc",
"server",
"store",
"traversable",
"types",
]
bencher = ["brk_bencher"]
bindgen = ["brk_bindgen"]
client = ["brk_client"]
computer = ["brk_computer"]
error = ["brk_error"]
fetcher = ["brk_fetcher"]
cohort = ["brk_cohort"]
indexer = ["brk_indexer"]
iterator = ["brk_iterator"]
logger = ["brk_logger"]
mempool = ["brk_mempool"]
query = ["brk_query"]
reader = ["brk_reader"]
rpc = ["brk_rpc"]
server = ["brk_server"]
store = ["brk_store"]
traversable = ["brk_traversable"]
types = ["brk_types"]
[dependencies]
brk_bencher = { workspace = true, optional = true }
brk_bindgen = { workspace = true, optional = true }
brk_client = { workspace = true, optional = true }
brk_computer = { workspace = true, optional = true }
brk_error = { workspace = true, optional = true }
brk_fetcher = { workspace = true, optional = true }
brk_cohort = { workspace = true, optional = true }
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_query = { workspace = true, optional = true }
brk_reader = { workspace = true, optional = true }
brk_rpc = { workspace = true, optional = true }
brk_server = { workspace = true, optional = true }
brk_store = { workspace = true, optional = true }
brk_traversable = { workspace = true, optional = true }
brk_types = { workspace = true, optional = true }
[package.metadata.docs.rs]
all-features = true
+62
View File
@@ -0,0 +1,62 @@
# brk
Umbrella crate for the Bitcoin Research Kit.
[crates.io](https://crates.io/crates/brk) | [docs.rs](https://docs.rs/brk)
## Usage
Single dependency to access any BRK component. Enable only what you need via feature flags.
```toml
[dependencies]
brk = { version = "0.1", features = ["query", "types"] }
```
```rust,ignore
use brk::query::Query;
use brk::types::Height;
```
Feature flags match crate names without the `brk_` prefix. Use `full` to enable all.
## Crates
**Core Pipeline**
| Crate | Description |
|-------|-------------|
| [brk_reader](https://docs.rs/brk_reader) | Read blocks from `blk*.dat` with parallel parsing and XOR decoding |
| [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_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 |
**Data & Storage**
| Crate | Description |
|-------|-------------|
| [brk_types](https://docs.rs/brk_types) | Domain types: `Height`, `Sats`, `Txid`, addresses, etc. |
| [brk_store](https://docs.rs/brk_store) | Key-value storage (fjall wrapper) |
| [brk_fetcher](https://docs.rs/brk_fetcher) | Fetch price data from exchanges |
| [brk_rpc](https://docs.rs/brk_rpc) | Bitcoin Core RPC client |
| [brk_iterator](https://docs.rs/brk_iterator) | Unified block iteration with automatic source selection |
| [brk_cohort](https://docs.rs/brk_cohort) | UTXO and address cohort filtering |
| [brk_traversable](https://docs.rs/brk_traversable) | Navigate hierarchical data structures |
**Clients & Integration**
| Crate | Description |
|-------|-------------|
| [brk_client](https://docs.rs/brk_client) | Generated Rust API client |
| [brk_bindgen](https://docs.rs/brk_bindgen) | Generate typed clients (Rust, JavaScript, Python) |
**Internal**
| Crate | Description |
|-------|-------------|
| [brk_cli](https://docs.rs/brk_cli) | CLI binary (`cargo install --locked brk_cli`) |
| [brk_error](https://docs.rs/brk_error) | Error types |
| [brk_logger](https://docs.rs/brk_logger) | Logging infrastructure |
| [brk_bencher](https://docs.rs/brk_bencher) | Benchmarking utilities |
+1
View File
@@ -0,0 +1 @@
sudo cargo flamegraph --profile profiling --root
+2
View File
@@ -0,0 +1,2 @@
cargo build --profile profiling
samply record ../../target/profiling/brk
+73
View File
@@ -0,0 +1,73 @@
#![doc = include_str!("../README.md")]
#[cfg(feature = "bencher")]
#[doc(inline)]
pub use brk_bencher as bencher;
#[cfg(feature = "bindgen")]
#[doc(inline)]
pub use brk_bindgen as bindgen;
#[cfg(feature = "client")]
#[doc(inline)]
pub use brk_client as client;
#[cfg(feature = "cohort")]
#[doc(inline)]
pub use brk_cohort as cohort;
#[cfg(feature = "computer")]
#[doc(inline)]
pub use brk_computer as computer;
#[cfg(feature = "error")]
#[doc(inline)]
pub use brk_error as error;
#[cfg(feature = "fetcher")]
#[doc(inline)]
pub use brk_fetcher as fetcher;
#[cfg(feature = "indexer")]
#[doc(inline)]
pub use brk_indexer as indexer;
#[cfg(feature = "iterator")]
#[doc(inline)]
pub use brk_iterator as iterator;
#[cfg(feature = "logger")]
#[doc(inline)]
pub use brk_logger as logger;
#[cfg(feature = "mempool")]
#[doc(inline)]
pub use brk_mempool as mempool;
#[cfg(feature = "query")]
#[doc(inline)]
pub use brk_query as query;
#[cfg(feature = "reader")]
#[doc(inline)]
pub use brk_reader as reader;
#[cfg(feature = "rpc")]
#[doc(inline)]
pub use brk_rpc as rpc;
#[cfg(feature = "server")]
#[doc(inline)]
pub use brk_server as server;
#[cfg(feature = "store")]
#[doc(inline)]
pub use brk_store as store;
#[cfg(feature = "traversable")]
#[doc(inline)]
pub use brk_traversable as traversable;
#[cfg(feature = "types")]
#[doc(inline)]
pub use brk_types as types;
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "brk_alloc"
description = "Global allocator and memory utilities for brk"
version.workspace = true
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
[dependencies]
libmimalloc-sys = { version = "0.1.44", features = ["extended"] }
mimalloc = { version = "0.1.48", features = ["v3"] }
+21
View File
@@ -0,0 +1,21 @@
//! Global allocator and memory utilities for brk.
//!
//! This crate sets mimalloc as the global allocator and provides
//! utilities for monitoring and managing memory.
use mimalloc::MiMalloc as Allocator;
#[global_allocator]
static GLOBAL: Allocator = Allocator;
/// Mimalloc allocator utilities
pub struct Mimalloc;
impl Mimalloc {
/// Eagerly free memory back to OS.
/// Only call at natural pause points.
#[inline]
pub fn collect() {
unsafe { libmimalloc_sys::mi_collect(true) }
}
}
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "brk_bencher"
description = "A simple benchmarker for testing other crates."
version.workspace = true
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
[dependencies]
brk_error = { workspace = true }
brk_logger = { workspace = true }
parking_lot = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
libproc = "0.14"
+43
View File
@@ -0,0 +1,43 @@
# brk_bencher
Resource monitoring for long-running Bitcoin indexing operations.
## What It Enables
Track disk usage, memory consumption (current + peak), and I/O throughput during indexing runs. Progress tracking hooks into brk_logger to record processing milestones automatically.
## Key Features
- **Multi-metric monitoring**: Disk, memory (RSS + peak), I/O read/write
- **Progress tracking**: Integrates with logging to capture block heights as they're processed
- **Run comparison**: Outputs timestamped CSVs for comparing multiple runs
- **macOS optimized**: Uses libproc for accurate process metrics on macOS
- **Non-blocking**: Monitors in background thread with 5-second sample interval
## Core API
```rust,ignore
let mut bencher = Bencher::from_cargo_env("brk_indexer", &data_path)?;
bencher.start()?;
// ... run indexing ...
bencher.stop()?;
```
## Output Structure
```
benches/
└── brk_indexer/
└── 1703001234/
├── disk.csv # timestamp_ms, bytes
├── memory.csv # timestamp_ms, current, peak
├── io.csv # timestamp_ms, read, written
└── progress.csv # timestamp_ms, height
```
## Built On
- `brk_error` for error handling
- `brk_logger` for progress hook integration
+66
View File
@@ -0,0 +1,66 @@
use std::{
collections::HashMap,
fs::{self, File},
io::{self, Write},
os::unix::fs::MetadataExt,
path::{Path, PathBuf},
time::SystemTime,
};
pub struct DiskMonitor {
cache: HashMap<PathBuf, (u64, SystemTime)>, // path -> (bytes_used, mtime)
monitored_path: PathBuf,
writer: File,
}
impl DiskMonitor {
pub fn new(monitored_path: &Path, csv_path: &Path) -> io::Result<Self> {
let mut writer = File::create(csv_path)?;
writeln!(writer, "timestamp_ms,disk_usage")?;
Ok(Self {
cache: HashMap::new(),
monitored_path: monitored_path.to_path_buf(),
writer,
})
}
/// Record disk usage at the given timestamp
pub fn record(&mut self, elapsed_ms: u128) -> io::Result<()> {
if let Ok(bytes) = self.scan_recursive(&self.monitored_path.clone()) {
writeln!(self.writer, "{},{}", elapsed_ms, bytes)?;
}
Ok(())
}
fn scan_recursive(&mut self, path: &Path) -> io::Result<u64> {
let mut total = 0;
for entry in fs::read_dir(path)? {
let entry = entry?;
let path = entry.path();
let metadata = entry.metadata()?;
if metadata.is_file() {
let mtime = metadata.modified()?;
// Check cache: if mtime unchanged, use cached value
if let Some((cached_bytes, cached_mtime)) = self.cache.get(&path)
&& *cached_mtime == mtime
{
total += cached_bytes;
continue;
}
// File is new or modified - get actual disk usage
let bytes = metadata.blocks() * 512;
self.cache.insert(path, (bytes, mtime));
total += bytes;
} else if metadata.is_dir() {
total += self.scan_recursive(&path)?;
}
}
Ok(total)
}
}
+83
View File
@@ -0,0 +1,83 @@
use std::{
fs::File,
io::{self, Write},
path::Path,
};
#[cfg(target_os = "linux")]
use std::fs;
#[cfg(target_os = "macos")]
use libproc::pid_rusage::{pidrusage, RUsageInfoV2};
pub struct IoMonitor {
pid: u32,
writer: File,
}
impl IoMonitor {
pub fn new(pid: u32, csv_path: &Path) -> io::Result<Self> {
let mut writer = File::create(csv_path)?;
writeln!(writer, "timestamp_ms,bytes_read,bytes_written")?;
Ok(Self { pid, writer })
}
/// Record I/O usage at the given timestamp
pub fn record(&mut self, elapsed_ms: u128) -> io::Result<()> {
if let Ok((read, written)) = self.get_io_usage() {
writeln!(self.writer, "{},{},{}", elapsed_ms, read, written)?;
}
Ok(())
}
/// Get I/O usage in bytes
/// Returns (bytes_read, bytes_written)
fn get_io_usage(&self) -> io::Result<(u64, u64)> {
#[cfg(target_os = "linux")]
{
self.get_io_usage_linux()
}
#[cfg(target_os = "macos")]
{
self.get_io_usage_macos()
}
}
#[cfg(target_os = "linux")]
fn get_io_usage_linux(&self) -> io::Result<(u64, u64)> {
let io_content = fs::read_to_string(format!("/proc/{}/io", self.pid))?;
let mut read_bytes = None;
let mut write_bytes = None;
for line in io_content.lines() {
if line.starts_with("read_bytes:") {
if let Some(value_str) = line.split_whitespace().nth(1) {
read_bytes = value_str.parse::<u64>().ok();
}
} else if line.starts_with("write_bytes:") {
if let Some(value_str) = line.split_whitespace().nth(1) {
write_bytes = value_str.parse::<u64>().ok();
}
}
}
match (read_bytes, write_bytes) {
(Some(r), Some(w)) => Ok((r, w)),
_ => Err(io::Error::new(
io::ErrorKind::InvalidData,
"Failed to parse I/O stats from /proc/[pid]/io",
)),
}
}
#[cfg(target_os = "macos")]
fn get_io_usage_macos(&self) -> io::Result<(u64, u64)> {
match pidrusage::<RUsageInfoV2>(self.pid as i32) {
Ok(info) => Ok((info.ri_diskio_bytesread, info.ri_diskio_byteswritten)),
Err(_) => Err(io::Error::other("Failed to get process I/O stats")),
}
}
}
+160
View File
@@ -0,0 +1,160 @@
use std::{
fs,
path::{Path, PathBuf},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
thread::{self, JoinHandle},
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
use brk_error::{Error, Result};
mod disk;
mod io;
mod memory;
mod progression;
use disk::*;
use io::*;
use memory::*;
use parking_lot::Mutex;
use progression::*;
#[derive(Clone)]
pub struct Bencher(Arc<BencherInner>);
struct BencherInner {
bench_dir: PathBuf,
monitored_path: PathBuf,
stop_flag: Arc<AtomicBool>,
monitor_thread: Mutex<Option<JoinHandle<Result<()>>>>,
progression: Arc<ProgressionMonitor>,
}
impl Bencher {
/// Create a new bencher for the given crate name
/// Creates directory structure: workspace_root/benches/{crate_name}/{timestamp}/
pub fn new(crate_name: &str, workspace_root: &Path, monitored_path: &Path) -> Result<Self> {
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
let bench_dir = workspace_root
.join("benches")
.join(crate_name)
.join(timestamp.to_string());
fs::create_dir_all(&bench_dir)?;
let progress_csv = bench_dir.join("progress.csv");
let progression = Arc::new(ProgressionMonitor::new(&progress_csv)?);
let progression_clone = progression.clone();
// Register hook with logger
brk_logger::register_hook(move |message| {
progression_clone.check_and_record(message);
})
.map_err(|e| std::io::Error::new(std::io::ErrorKind::AlreadyExists, e))?;
Ok(Self(Arc::new(BencherInner {
bench_dir,
monitored_path: monitored_path.to_path_buf(),
stop_flag: Arc::new(AtomicBool::new(false)),
progression,
monitor_thread: Mutex::new(None),
})))
}
/// Create a bencher using CARGO_MANIFEST_DIR to find workspace root
pub fn from_cargo_env(crate_name: &str, monitored_path: &Path) -> Result<Self> {
let mut current = std::env::current_dir()
.map_err(|e| format!("Failed to get current directory: {}", e))
.unwrap();
let workspace_root = loop {
let cargo_toml = current.join("Cargo.toml");
if cargo_toml.exists() {
let contents = std::fs::read_to_string(&cargo_toml)
.map_err(|e| format!("Failed to read Cargo.toml: {}", e))
.unwrap();
if contents.contains("[workspace]") {
break current;
}
}
current = current
.parent()
.ok_or(Error::NotFound("Workspace root not found".into()))?
.to_path_buf();
};
Self::new(crate_name, &workspace_root, monitored_path)
}
/// Start monitoring disk usage and memory footprint
pub fn start(&mut self) -> Result<()> {
if self.0.monitor_thread.lock().is_some() {
return Err(Error::Internal("Bencher already started"));
}
let stop_flag = self.0.stop_flag.clone();
let bench_dir = self.0.bench_dir.clone();
let monitored_path = self.0.monitored_path.clone();
let handle =
thread::spawn(move || monitor_resources(&monitored_path, &bench_dir, stop_flag));
*self.0.monitor_thread.lock() = Some(handle);
Ok(())
}
/// Stop monitoring and wait for the thread to finish
pub fn stop(&self) -> Result<()> {
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"))??;
}
self.0.progression.flush()?;
Ok(())
}
}
impl Drop for Bencher {
fn drop(&mut self) {
let _ = self.stop();
}
}
fn monitor_resources(
monitored_path: &Path,
bench_dir: &Path,
stop_flag: Arc<AtomicBool>,
) -> Result<()> {
let pid = std::process::id();
let start = Instant::now();
let mut disk_monitor = DiskMonitor::new(monitored_path, &bench_dir.join("disk.csv"))?;
let mut memory_monitor = MemoryMonitor::new(pid, &bench_dir.join("memory.csv"))?;
let mut io_monitor = IoMonitor::new(pid, &bench_dir.join("io.csv"))?;
'l: loop {
let elapsed_ms = start.elapsed().as_millis();
disk_monitor.record(elapsed_ms)?;
memory_monitor.record(elapsed_ms)?;
io_monitor.record(elapsed_ms)?;
for _ in 0..50 {
// 50 * 100ms = 5 seconds
if stop_flag.load(Ordering::Relaxed) {
break 'l;
}
thread::sleep(Duration::from_millis(100));
}
}
Ok(())
}
+143
View File
@@ -0,0 +1,143 @@
use std::{
fs::File,
io::{self, Write},
path::Path,
};
#[cfg(target_os = "linux")]
use std::fs;
#[cfg(target_os = "macos")]
use std::process::Command;
pub struct MemoryMonitor {
pid: u32,
writer: File,
}
impl MemoryMonitor {
pub fn new(pid: u32, csv_path: &Path) -> io::Result<Self> {
let mut writer = File::create(csv_path)?;
writeln!(writer, "timestamp_ms,phys_footprint,phys_footprint_peak")?;
Ok(Self { pid, writer })
}
/// Record memory usage at the given timestamp
pub fn record(&mut self, elapsed_ms: u128) -> io::Result<()> {
if let Ok((footprint, peak)) = self.get_memory_usage() {
writeln!(self.writer, "{},{},{}", elapsed_ms, footprint, peak)?;
}
Ok(())
}
/// Get memory usage in bytes
/// Returns (current_bytes, peak_bytes)
fn get_memory_usage(&self) -> io::Result<(u64, u64)> {
#[cfg(target_os = "linux")]
{
self.get_memory_usage_linux()
}
#[cfg(target_os = "macos")]
{
self.get_memory_usage_macos()
}
}
#[cfg(target_os = "linux")]
fn get_memory_usage_linux(&self) -> io::Result<(u64, u64)> {
let status_content = fs::read_to_string(format!("/proc/{}/status", self.pid))?;
let mut vm_rss = None;
let mut vm_hwm = None;
for line in status_content.lines() {
if line.starts_with("VmRSS:") {
if let Some(value_str) = line.split_whitespace().nth(1) {
if let Ok(kb) = value_str.parse::<u64>() {
vm_rss = Some(kb * 1024); // KiB to bytes
}
}
} else if line.starts_with("VmHWM:") {
if let Some(value_str) = line.split_whitespace().nth(1) {
if let Ok(kb) = value_str.parse::<u64>() {
vm_hwm = Some(kb * 1024); // KiB to bytes
}
}
}
}
match (vm_rss, vm_hwm) {
(Some(rss), Some(hwm)) => Ok((rss, hwm)),
_ => Err(io::Error::new(
io::ErrorKind::InvalidData,
"Failed to parse memory info from /proc/[pid]/status",
)),
}
}
#[cfg(target_os = "macos")]
fn get_memory_usage_macos(&self) -> io::Result<(u64, u64)> {
let output = Command::new("footprint")
.args(["-p", &self.pid.to_string()])
.output()?;
let stdout = String::from_utf8(output.stdout).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-8 from footprint")
})?;
parse_footprint_output(&stdout).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"Failed to parse footprint output",
)
})
}
}
#[cfg(target_os = "macos")]
fn parse_footprint_output(output: &str) -> Option<(u64, u64)> {
let mut phys_footprint = None;
let mut phys_footprint_peak = None;
for line in output.lines() {
let line = line.trim();
if line.starts_with("phys_footprint:") {
// Format: "phys_footprint: 7072 KB"
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
// parts[0] = "phys_footprint:"
// parts[1] = "7072"
// parts[2] = "KB"
phys_footprint = parse_size_to_bytes(parts[1], parts[2]);
}
} else if line.starts_with("phys_footprint_peak:") {
// Format: "phys_footprint_peak: 15 MB"
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
phys_footprint_peak = parse_size_to_bytes(parts[1], parts[2]);
}
}
}
match (phys_footprint, phys_footprint_peak) {
(Some(f), Some(p)) => Some((f, p)),
_ => None,
}
}
#[cfg(target_os = "macos")]
fn parse_size_to_bytes(value: &str, unit: &str) -> Option<u64> {
let value: f64 = value.parse().ok()?;
let multiplier = match unit.to_uppercase().as_str() {
"KB" => 1024.0, // KiB to bytes
"MB" => 1024.0 * 1024.0, // MiB to bytes
"GB" => 1024.0 * 1024.0 * 1024.0, // GiB to bytes
_ => return None,
};
Some((value * multiplier) as u64)
}
+74
View File
@@ -0,0 +1,74 @@
use parking_lot::Mutex;
use std::{
fs,
io::{self, BufWriter, Write},
path::Path,
time::Instant,
};
/// Patterns to match for progress tracking.
const PROGRESS_PATTERNS: &[&str] = &[
"block ", // "Indexing block 123..."
"chain at ", // "Processing chain at 456..."
];
pub struct ProgressionMonitor {
csv_file: Mutex<BufWriter<fs::File>>,
start_time: Instant,
}
impl ProgressionMonitor {
pub fn new(csv_path: &Path) -> io::Result<Self> {
let mut csv_file = BufWriter::new(fs::File::create(csv_path)?);
writeln!(csv_file, "timestamp_ms,value")?;
Ok(Self {
csv_file: Mutex::new(csv_file),
start_time: Instant::now(),
})
}
/// Check message for progress patterns and record if found
#[inline]
pub fn check_and_record(&self, message: &str) {
let Some(value) = parse_progress(message) else {
return;
};
if value % 10 != 0 {
return;
}
let elapsed_ms = self.start_time.elapsed().as_millis();
let _ = writeln!(self.csv_file.lock(), "{},{}", elapsed_ms, value);
}
pub fn flush(&self) -> io::Result<()> {
self.csv_file.lock().flush()
}
}
/// Parse progress value from message
#[inline]
fn parse_progress(message: &str) -> Option<u64> {
PROGRESS_PATTERNS
.iter()
.find_map(|pattern| parse_number_after(message, pattern))
}
/// Extract number immediately following the pattern
#[inline]
fn parse_number_after(message: &str, pattern: &str) -> Option<u64> {
let start = message.find(pattern)?;
let after = &message[start + pattern.len()..];
let end = after
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(after.len());
if end == 0 {
return None;
}
after[..end].parse().ok()
}
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "brk_bencher_visualizer"
description = "A generator of charts for brk_bencher"
version.workspace = true
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
publish = false
[dependencies]
plotters = "0.3.7"
+34
View File
@@ -0,0 +1,34 @@
# brk_bencher_visualizer
SVG chart generation for benchmark visualization.
## What It Enables
Turn benchmark CSV data into publication-ready SVG charts showing disk usage, memory (current/peak), progress, and I/O over time. Compare multiple runs side-by-side with automatic color coding.
## Key Features
- **Multi-run comparison**: Overlay multiple benchmark runs with distinct colors
- **Dual-axis charts**: Memory charts show both current and peak usage (solid vs dashed lines)
- **Smart scaling**: Automatic unit conversion for bytes (KB/MB/GB) and time (seconds/minutes/hours)
- **Per-run trimming**: Aligns data by progress cutoffs for fair comparison
- **Dark theme**: Clean, readable charts with monospace fonts
## Core API
```rust,ignore
let viz = Visualizer::from_cargo_env()?;
viz.generate_all_charts()?; // Process all crates in benches/
```
## Chart Types
- `disk.svg` - Storage consumption over time
- `memory.svg` - Current + peak memory usage
- `progress.svg` - Processing progress (e.g., blocks indexed)
- `io_read.svg` / `io_write.svg` - I/O throughput
## Input Format
Reads CSV files from `benches/<crate>/<run_id>/`:
- `disk.csv`, `memory.csv`, `progress.csv`, `io.csv`
+251
View File
@@ -0,0 +1,251 @@
use crate::data::{DataPoint, DualRun, Result, Run};
use crate::format;
use plotters::prelude::*;
use std::path::Path;
const FONT: &str = "monospace";
const FONT_SIZE: i32 = 20;
const FONT_SIZE_BIG: i32 = 30;
const SIZE: (u32, u32) = (2000, 1000);
const TIME_BUFFER_MS: u64 = 10_000;
const BG_COLOR: RGBColor = RGBColor(18, 18, 24);
const TEXT_COLOR: RGBColor = RGBColor(230, 230, 240);
const COLORS: [RGBColor; 6] = [
RGBColor(255, 99, 132), // Pink/Red
RGBColor(54, 162, 235), // Blue
RGBColor(75, 192, 192), // Teal
RGBColor(255, 206, 86), // Yellow
RGBColor(153, 102, 255), // Purple
RGBColor(255, 159, 64), // Orange
];
pub enum YAxisFormat {
Bytes,
Number,
}
pub struct ChartConfig<'a> {
pub output_path: &'a Path,
pub title: String,
pub y_label: String,
pub y_format: YAxisFormat,
}
/// Generate a simple line chart from runs
pub fn generate(config: ChartConfig, runs: &[Run]) -> Result<()> {
if runs.is_empty() {
return Ok(());
}
let max_time_ms = runs.iter().map(|r| r.max_timestamp()).max().unwrap_or(1000) + TIME_BUFFER_MS;
let max_time_s = max_time_ms as f64 / 1000.0;
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 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))
.margin(20)
.margin_right(40)
.x_label_area_size(50)
.margin_left(50)
.right_y_label_area_size(75)
.build_cartesian_2d(0.0..time_scaled * 1.025, 0.0..value_scaled * 1.1)?;
configure_mesh(&mut chart, time_label, &y_label, &config.y_format, x_labels)?;
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)?;
}
configure_legend(&mut chart)?;
root.present()?;
println!("Generated: {}", config.output_path.display());
Ok(())
}
/// Generate a chart with dual series per run (e.g., current + peak memory)
pub fn generate_dual(
config: ChartConfig,
runs: &[DualRun],
primary_suffix: &str,
secondary_suffix: &str,
) -> Result<()> {
if runs.is_empty() {
return Ok(());
}
let max_time_ms = runs
.iter()
.flat_map(|r| r.primary.iter().chain(r.secondary.iter()))
.map(|d| d.timestamp_ms)
.max()
.unwrap_or(1000)
+ TIME_BUFFER_MS;
let max_time_s = max_time_ms as f64 / 1000.0;
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 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))
.margin(20)
.margin_right(40)
.x_label_area_size(50)
.margin_left(50)
.right_y_label_area_size(75)
.build_cartesian_2d(0.0..time_scaled * 1.025, 0.0..value_scaled * 1.1)?;
configure_mesh(&mut chart, time_label, &y_label, &config.y_format, x_labels)?;
for (idx, run) in runs.iter().enumerate() {
let color = COLORS[idx % COLORS.len()];
// Primary series (solid)
draw_series(
&mut chart,
&run.primary,
&format!("{} {}", run.id, primary_suffix),
color,
time_divisor,
scale_factor,
)?;
// Secondary series (dashed)
draw_dashed_series(
&mut chart,
&run.secondary,
&format!("{} {}", run.id, secondary_suffix),
color.mix(0.5),
time_divisor,
scale_factor,
)?;
}
configure_legend(&mut chart)?;
root.present()?;
println!("Generated: {}", config.output_path.display());
Ok(())
}
fn scale_y_axis(max_value: f64, base_label: &str, y_format: &YAxisFormat) -> (f64, f64, String) {
match y_format {
YAxisFormat::Bytes => {
let (scaled, unit) = format::bytes(max_value);
let factor = max_value / scaled;
(scaled, factor, format!("{} ({})", base_label, unit))
}
YAxisFormat::Number => (max_value, 1.0, base_label.to_string()),
}
}
/// Calculate appropriate label count to avoid duplicates when rounding to integers
fn label_count(max_value: f64) -> usize {
let max_int = max_value.ceil() as usize;
// Don't exceed the range, cap at 12 for readability
max_int.clamp(2, 12)
}
type Chart<'a, 'b> = ChartContext<
'a,
SVGBackend<'b>,
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<()> {
let y_formatter: Box<dyn Fn(&f64) -> String> = match y_format {
YAxisFormat::Bytes => Box::new(|y: &f64| {
if y.fract() == 0.0 {
format!("{:.0}", y)
} else {
format!("{:.1}", y)
}
}),
YAxisFormat::Number => Box::new(|y: &f64| format::axis_number(*y)),
};
chart
.configure_mesh()
.disable_mesh()
.x_desc(x_label)
.y_desc(y_label)
.x_label_formatter(&|x| format!("{:.0}", x))
.y_label_formatter(&y_formatter)
.x_labels(x_labels)
.y_labels(10)
.x_label_style((FONT, FONT_SIZE).into_font().color(&TEXT_COLOR.mix(0.7)))
.y_label_style((FONT, FONT_SIZE).into_font().color(&TEXT_COLOR.mix(0.7)))
.axis_style(TEXT_COLOR.mix(0.3))
.draw()?;
Ok(())
}
fn draw_series(
chart: &mut Chart,
data: &[DataPoint],
label: &str,
color: RGBColor,
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));
chart
.draw_series(LineSeries::new(points, color.stroke_width(1)))?
.label(label)
.legend(move |(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], color.stroke_width(1)));
Ok(())
}
fn draw_dashed_series(
chart: &mut Chart,
data: &[DataPoint],
label: &str,
color: RGBAColor,
time_divisor: f64,
scale_factor: f64,
) -> Result<()> {
let points: Vec<_> = data
.iter()
.map(|d| (d.timestamp_ms as f64 / 1000.0 / time_divisor, d.value / scale_factor))
.collect();
// Draw dashed line by skipping every other segment
chart
.draw_series(
points
.windows(2)
.enumerate()
.filter(|(i, _)| i % 2 == 0)
.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)));
Ok(())
}
fn configure_legend<'a>(chart: &mut Chart<'a, 'a>) -> Result<()> {
chart
.configure_series_labels()
.position(SeriesLabelPosition::UpperLeft)
.label_font((FONT, FONT_SIZE).into_font().color(&TEXT_COLOR.mix(0.9)))
.background_style(BG_COLOR.mix(0.98))
.border_style(BG_COLOR)
.margin(10)
.draw()?;
Ok(())
}
+238
View File
@@ -0,0 +1,238 @@
use std::{collections::HashMap, fs, path::Path};
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
#[derive(Debug, Clone)]
pub struct DataPoint {
pub timestamp_ms: u64,
pub value: f64,
}
/// Per-run cutoff timestamps for fair comparison
pub struct Cutoffs {
by_id: HashMap<String, u64>,
default: u64,
}
impl Cutoffs {
/// Calculate cutoffs from progress runs.
/// Finds the common max progress, then returns when each run reached it.
pub fn from_progress(progress_runs: &[Run]) -> Self {
const TIME_BUFFER_MS: u64 = 10_000;
if progress_runs.is_empty() {
return Self {
by_id: HashMap::new(),
default: u64::MAX,
};
}
// Find the minimum of max progress values (the common point all runs reached)
let common_progress = progress_runs
.iter()
.map(|r| r.max_value())
.fold(f64::MAX, f64::min);
let by_id: HashMap<_, _> = progress_runs
.iter()
.map(|run| {
let cutoff = run
.data
.iter()
.find(|d| d.value >= common_progress)
.map(|d| d.timestamp_ms)
.unwrap_or_else(|| run.max_timestamp())
.saturating_add(TIME_BUFFER_MS);
(run.id.clone(), cutoff)
})
.collect();
let default = by_id.values().copied().max().unwrap_or(u64::MAX);
Self { by_id, default }
}
pub fn get(&self, id: &str) -> u64 {
self.by_id.get(id).copied().unwrap_or(self.default)
}
pub fn trim_runs(&self, runs: &[Run]) -> Vec<Run> {
runs.iter().map(|r| r.trimmed(self.get(&r.id))).collect()
}
pub fn trim_dual_runs(&self, runs: &[DualRun]) -> Vec<DualRun> {
runs.iter().map(|r| r.trimmed(self.get(&r.id))).collect()
}
}
#[derive(Debug, Clone)]
pub struct Run {
pub id: String,
pub data: Vec<DataPoint>,
}
impl Run {
pub fn max_timestamp(&self) -> u64 {
self.data.iter().map(|d| d.timestamp_ms).max().unwrap_or(0)
}
pub fn max_value(&self) -> f64 {
self.data.iter().map(|d| d.value).fold(0.0, f64::max)
}
pub fn trimmed(&self, max_timestamp_ms: u64) -> Self {
Self {
id: self.id.clone(),
data: self
.data
.iter()
.filter(|d| d.timestamp_ms <= max_timestamp_ms)
.cloned()
.collect(),
}
}
}
/// Two data series from a single run (e.g., memory footprint + peak, or io read + write)
#[derive(Debug, Clone)]
pub struct DualRun {
pub id: String,
pub primary: Vec<DataPoint>,
pub secondary: Vec<DataPoint>,
}
impl DualRun {
pub fn trimmed(&self, max_timestamp_ms: u64) -> Self {
Self {
id: self.id.clone(),
primary: self
.primary
.iter()
.filter(|d| d.timestamp_ms <= max_timestamp_ms)
.cloned()
.collect(),
secondary: self
.secondary
.iter()
.filter(|d| d.timestamp_ms <= max_timestamp_ms)
.cloned()
.collect(),
}
}
pub fn max_value(&self) -> f64 {
self.primary
.iter()
.chain(self.secondary.iter())
.map(|d| d.value)
.fold(0.0, f64::max)
}
}
pub fn read_runs(crate_path: &Path, filename: &str) -> Result<Vec<Run>> {
let mut runs = Vec::new();
for entry in fs::read_dir(crate_path)? {
let run_path = entry?.path();
if !run_path.is_dir() {
continue;
}
let run_id = run_path
.file_name()
.and_then(|n| n.to_str())
.ok_or("Invalid run ID")?
.to_string();
// Skip underscore-prefixed or numeric-only directories
if run_id.starts_with('_') || run_id.chars().all(|c| c.is_ascii_digit()) {
continue;
}
let csv_path = run_path.join(filename);
if csv_path.exists()
&& let Ok(data) = read_csv(&csv_path)
{
runs.push(Run { id: run_id, data });
}
}
Ok(runs)
}
pub fn read_dual_runs(crate_path: &Path, filename: &str) -> Result<Vec<DualRun>> {
let mut runs = Vec::new();
for entry in fs::read_dir(crate_path)? {
let run_path = entry?.path();
if !run_path.is_dir() {
continue;
}
let run_id = run_path
.file_name()
.and_then(|n| n.to_str())
.ok_or("Invalid run ID")?
.to_string();
if run_id.starts_with('_') || run_id.chars().all(|c| c.is_ascii_digit()) {
continue;
}
let csv_path = run_path.join(filename);
if csv_path.exists()
&& let Ok((primary, secondary)) = read_dual_csv(&csv_path)
{
runs.push(DualRun {
id: run_id,
primary,
secondary,
});
}
}
Ok(runs)
}
fn read_csv(path: &Path) -> Result<Vec<DataPoint>> {
let content = fs::read_to_string(path)?;
let data = content
.lines()
.skip(1) // header
.filter_map(|line| {
let mut parts = line.split(',');
let timestamp_ms = parts.next()?.parse().ok()?;
let value = parts.next()?.parse().ok()?;
Some(DataPoint {
timestamp_ms,
value,
})
})
.collect();
Ok(data)
}
fn read_dual_csv(path: &Path) -> Result<(Vec<DataPoint>, Vec<DataPoint>)> {
let content = fs::read_to_string(path)?;
let mut primary = Vec::new();
let mut secondary = Vec::new();
for line in content.lines().skip(1) {
let mut parts = line.split(',');
if let (Some(ts), Some(v1), Some(v2)) = (parts.next(), parts.next(), parts.next())
&& let (Ok(timestamp_ms), Ok(val1), Ok(val2)) =
(ts.parse(), v1.parse::<f64>(), v2.parse::<f64>())
{
primary.push(DataPoint {
timestamp_ms,
value: val1,
});
secondary.push(DataPoint {
timestamp_ms,
value: val2,
});
}
}
Ok((primary, secondary))
}
@@ -0,0 +1,45 @@
const KIB: f64 = 1024.0;
const MIB: f64 = KIB * 1024.0;
const GIB: f64 = MIB * 1024.0;
const MINUTE: f64 = 60.0;
const HOUR: f64 = 3600.0;
/// Returns (scaled_value, unit_suffix)
pub fn bytes(bytes: f64) -> (f64, &'static str) {
if bytes >= GIB {
(bytes / GIB, "GiB")
} else if bytes >= MIB {
(bytes / MIB, "MiB")
} else if bytes >= KIB {
(bytes / KIB, "KiB")
} else {
(bytes, "bytes")
}
}
/// Returns (scaled_value, divisor, axis_label)
pub fn time(seconds: f64) -> (f64, f64, &'static str) {
if seconds >= HOUR * 2.0 {
(seconds / HOUR, HOUR, "Time (h)")
} else if seconds >= MINUTE * 2.0 {
(seconds / MINUTE, MINUTE, "Time (min)")
} else {
(seconds, 1.0, "Time (s)")
}
}
pub fn axis_number(value: f64) -> String {
if value >= 1000.0 {
let k = value / 1000.0;
if k.fract() == 0.0 || k >= 100.0 {
format!("{:.0}k", k)
} else if k >= 10.0 {
format!("{:.1}k", k)
} else {
format!("{:.2}k", k)
}
} else {
format!("{:.0}", value)
}
}
+249
View File
@@ -0,0 +1,249 @@
mod chart;
mod data;
mod format;
use data::{read_dual_runs, read_runs, Cutoffs, DualRun, Result, Run};
use std::{
fs,
path::{Path, PathBuf},
};
pub struct Visualizer {
workspace_root: PathBuf,
}
impl Visualizer {
pub fn new(workspace_root: impl AsRef<Path>) -> Self {
Self {
workspace_root: workspace_root.as_ref().to_path_buf(),
}
}
pub fn from_cargo_env() -> Result<Self> {
let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.ok_or("Failed to find workspace root")?
.to_path_buf();
Ok(Self { workspace_root })
}
pub fn generate_all_charts(&self) -> Result<()> {
let benches_dir = self.workspace_root.join("benches");
if !benches_dir.exists() {
return Err("Benches directory does not exist".into());
}
for entry in fs::read_dir(&benches_dir)? {
let path = entry?.path();
if path.is_dir() {
let crate_name = path
.file_name()
.and_then(|n| n.to_str())
.ok_or("Invalid crate name")?;
println!("Generating charts for crate: {}", crate_name);
self.generate_crate_charts(&path, crate_name)?;
}
}
Ok(())
}
fn generate_crate_charts(&self, crate_path: &Path, crate_name: &str) -> Result<()> {
let disk_runs = read_runs(crate_path, "disk.csv")?;
let memory_runs = read_dual_runs(crate_path, "memory.csv")?;
let progress_runs = read_runs(crate_path, "progress.csv")?;
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)?;
// Individual charts (one per run)
self.generate_individual_charts(crate_path, crate_name, &disk_runs, &memory_runs, &progress_runs, &io_runs)?;
Ok(())
}
fn generate_combined_charts(
&self,
crate_path: &Path,
crate_name: &str,
disk_runs: &[Run],
memory_runs: &[DualRun],
progress_runs: &[Run],
io_runs: &[DualRun],
) -> Result<()> {
let cutoffs = Cutoffs::from_progress(progress_runs);
// Trim data to per-run cutoffs for fair comparison
let disk_trimmed = cutoffs.trim_runs(disk_runs);
let memory_trimmed = cutoffs.trim_dual_runs(memory_runs);
let io_trimmed = cutoffs.trim_dual_runs(io_runs);
if !disk_trimmed.is_empty() {
chart::generate(
chart::ChartConfig {
output_path: &crate_path.join("disk.svg"),
title: format!("{} — Disk Usage", crate_name),
y_label: "Disk Usage".to_string(),
y_format: chart::YAxisFormat::Bytes,
},
&disk_trimmed,
)?;
}
if !memory_trimmed.is_empty() {
chart::generate_dual(
chart::ChartConfig {
output_path: &crate_path.join("memory.svg"),
title: format!("{} — Memory", crate_name),
y_label: "Memory".to_string(),
y_format: chart::YAxisFormat::Bytes,
},
&memory_trimmed,
"(current)",
"(peak)",
)?;
}
if !progress_runs.is_empty() {
let progress_trimmed = cutoffs.trim_runs(progress_runs);
chart::generate(
chart::ChartConfig {
output_path: &crate_path.join("progress.svg"),
title: format!("{} — Progress", crate_name),
y_label: "Progress".to_string(),
y_format: chart::YAxisFormat::Number,
},
&progress_trimmed,
)?;
}
if !io_trimmed.is_empty() {
// I/O Read (primary column)
let io_read: Vec<_> = io_trimmed
.iter()
.map(|r| Run {
id: r.id.clone(),
data: r.primary.clone(),
})
.collect();
chart::generate(
chart::ChartConfig {
output_path: &crate_path.join("io_read.svg"),
title: format!("{} — I/O Read", crate_name),
y_label: "Bytes Read".to_string(),
y_format: chart::YAxisFormat::Bytes,
},
&io_read,
)?;
// I/O Write (secondary column)
let io_write: Vec<_> = io_trimmed
.iter()
.map(|r| Run {
id: r.id.clone(),
data: r.secondary.clone(),
})
.collect();
chart::generate(
chart::ChartConfig {
output_path: &crate_path.join("io_write.svg"),
title: format!("{} — I/O Write", crate_name),
y_label: "Bytes Written".to_string(),
y_format: chart::YAxisFormat::Bytes,
},
&io_write,
)?;
}
Ok(())
}
fn generate_individual_charts(
&self,
crate_path: &Path,
crate_name: &str,
disk_runs: &[Run],
memory_runs: &[DualRun],
progress_runs: &[Run],
io_runs: &[DualRun],
) -> Result<()> {
for run in disk_runs {
let run_path = crate_path.join(&run.id);
chart::generate(
chart::ChartConfig {
output_path: &run_path.join("disk.svg"),
title: format!("{} — Disk Usage", crate_name),
y_label: "Disk Usage".to_string(),
y_format: chart::YAxisFormat::Bytes,
},
std::slice::from_ref(run),
)?;
}
for run in memory_runs {
let run_path = crate_path.join(&run.id);
chart::generate_dual(
chart::ChartConfig {
output_path: &run_path.join("memory.svg"),
title: format!("{} — Memory", crate_name),
y_label: "Memory".to_string(),
y_format: chart::YAxisFormat::Bytes,
},
std::slice::from_ref(run),
"(current)",
"(peak)",
)?;
}
for run in progress_runs {
let run_path = crate_path.join(&run.id);
chart::generate(
chart::ChartConfig {
output_path: &run_path.join("progress.svg"),
title: format!("{} — Progress", crate_name),
y_label: "Progress".to_string(),
y_format: chart::YAxisFormat::Number,
},
std::slice::from_ref(run),
)?;
}
for run in io_runs {
let run_path = crate_path.join(&run.id);
let read_run = Run {
id: run.id.clone(),
data: run.primary.clone(),
};
chart::generate(
chart::ChartConfig {
output_path: &run_path.join("io_read.svg"),
title: format!("{} — I/O Read", crate_name),
y_label: "Bytes Read".to_string(),
y_format: chart::YAxisFormat::Bytes,
},
std::slice::from_ref(&read_run),
)?;
let write_run = Run {
id: run.id.clone(),
data: run.secondary.clone(),
};
chart::generate(
chart::ChartConfig {
output_path: &run_path.join("io_write.svg"),
title: format!("{} — I/O Write", crate_name),
y_label: "Bytes Written".to_string(),
y_format: chart::YAxisFormat::Bytes,
},
std::slice::from_ref(&write_run),
)?;
}
Ok(())
}
}
@@ -0,0 +1,6 @@
use brk_bencher_visualizer::Visualizer;
fn main() {
let v = Visualizer::from_cargo_env().unwrap();
v.generate_all_charts().unwrap();
}
+6
View File
@@ -0,0 +1,6 @@
clients/
/*.json
/*.js
/*.rs
/*.py
tests/output/
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "brk_bindgen"
description = "A trait-based generator of client bindings for multiple languages"
version.workspace = true
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
[dependencies]
brk_cohort = { workspace = true }
brk_query = { workspace = true }
brk_types = { workspace = true }
oas3 = "0.20"
serde = { workspace = true }
serde_json = { workspace = true }
+46
View File
@@ -0,0 +1,46 @@
# brk_bindgen
Code generation for BRK client libraries.
## What It Enables
Generate typed client libraries for Rust, JavaScript, and Python from the OpenAPI specification. Keeps frontend code in sync with available metrics and API endpoints without manual maintenance.
## Key Features
- **Multi-language**: Generates Rust, JavaScript, and Python clients
- **OpenAPI-driven**: Extracts endpoints and schemas from the OpenAPI spec
- **Metric catalog**: Includes all metric IDs and their supported indexes
- **Type definitions**: Generates types/interfaces from JSON Schema
- **Selective output**: Generate only the languages you need
## Core API
```rust,ignore
use brk_bindgen::{generate_clients, ClientOutputPaths};
let paths = ClientOutputPaths::new()
.rust("crates/brk_client/src/lib.rs")
.javascript("modules/brk-client/index.js")
.python("packages/brk_client/brk_client/__init__.py");
generate_clients(&vecs, &openapi_json, &paths)?;
```
## Generated Clients
| Language | Contents |
|----------|----------|
| Rust | Typed API client using `brk_types`, metric catalog |
| JavaScript | ES module with JSDoc types, metric catalog, fetch helpers |
| Python | Typed client with dataclasses, metric catalog |
Each client includes:
- All REST API endpoints as typed functions
- Complete metric catalog with index information
- Type definitions for request/response schemas
## Built On
- `brk_query` for metric enumeration
- `brk_types` for type schemas
+14
View File
@@ -0,0 +1,14 @@
//! Analysis module for name deconstruction and pattern detection.
//!
//! This module implements bottom-up analysis of vec names to detect
//! common denominators (prefixes/suffixes) and field positions.
mod names;
mod patterns;
mod positions;
mod tree;
pub use names::*;
pub use patterns::*;
pub use positions::*;
pub use tree::*;
+195
View File
@@ -0,0 +1,195 @@
//! Common prefix/suffix detection for metric names.
//!
//! This module provides utilities to find common prefixes and suffixes
//! among metric 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.
/// Returns None if no common prefix exists.
pub fn find_common_prefix(names: &[&str]) -> Option<String> {
if names.is_empty() || names.iter().any(|n| n.is_empty()) {
return None;
}
let first = names[0];
// Find character-by-character common prefix
let mut prefix_len = 0;
for (i, ch) in first.chars().enumerate() {
if names.iter().all(|n| n.chars().nth(i) == Some(ch)) {
prefix_len = i + 1;
} else {
break;
}
}
if prefix_len == 0 {
return None;
}
let raw_prefix = &first[..prefix_len];
// Must end at underscore boundary for semantic coherence
if raw_prefix.ends_with('_') {
return Some(raw_prefix.to_string());
}
// If raw_prefix equals one of the full names (one name is a prefix of all others),
// return it with trailing underscore for proper base detection
if names.contains(&raw_prefix) {
return Some(format!("{}_", raw_prefix));
}
// Find the last underscore position
if let Some(last_underscore) = raw_prefix.rfind('_') {
let clean_prefix = &first[..=last_underscore];
if names.iter().all(|n| n.starts_with(clean_prefix)) {
return Some(clean_prefix.to_string());
}
}
None
}
/// Find the longest common suffix among all strings.
/// Returns the suffix WITH leading underscore if found at word boundary.
/// Returns None if no common suffix exists.
pub fn find_common_suffix(names: &[&str]) -> Option<String> {
if names.is_empty() || names.iter().any(|n| n.is_empty()) {
return None;
}
let first = names[0];
let first_chars: Vec<char> = first.chars().collect();
// Find character-by-character common suffix (from the end)
let mut suffix_len = 0;
for i in 0..first_chars.len() {
let idx_from_end = first_chars.len() - 1 - i;
let ch = first_chars[idx_from_end];
let all_match = names.iter().all(|n| {
let n_chars: Vec<char> = n.chars().collect();
if i >= n_chars.len() {
return false;
}
n_chars[n_chars.len() - 1 - i] == ch
});
if all_match {
suffix_len = i + 1;
} else {
break;
}
}
if suffix_len == 0 {
return None;
}
let raw_suffix = &first[first.len() - suffix_len..];
// Must start at underscore boundary for semantic coherence
if raw_suffix.starts_with('_') {
return Some(raw_suffix.to_string());
}
// Check if preceded by underscore in all names (word boundary)
let at_word_boundary = names.iter().all(|n| {
if *n == raw_suffix {
true // Suffix is the whole string
} else if let Some(prefix) = n.strip_suffix(raw_suffix) {
prefix.ends_with('_')
} else {
false
}
});
if at_word_boundary {
return Some(format!("_{}", raw_suffix));
}
// Find the first underscore position in suffix
if let Some(first_underscore) = raw_suffix.find('_') {
let clean_suffix = &raw_suffix[first_underscore..];
if names.iter().all(|n| n.ends_with(clean_suffix)) {
return Some(clean_suffix.to_string());
}
}
None
}
/// Normalize a prefix string by ensuring it ends with underscore.
/// Returns empty string if input is empty.
pub fn normalize_prefix(s: &str) -> String {
if s.is_empty() {
String::new()
} else if s.ends_with('_') {
s.to_string()
} else {
format!("{}_", s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_common_prefix_basic() {
let names = vec!["addrs_0sats", "addrs_1sats", "addrs_2sats"];
assert_eq!(find_common_prefix(&names), Some("addrs_".to_string()));
}
#[test]
fn test_common_prefix_none() {
let names = vec!["foo", "bar", "baz"];
assert_eq!(find_common_prefix(&names), None);
}
#[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()));
}
#[test]
fn test_common_suffix_basic() {
let names = vec!["cumulative_supply", "net_supply", "total_supply"];
assert_eq!(find_common_suffix(&names), Some("_supply".to_string()));
}
#[test]
fn test_common_prefix_cost_basis() {
// With suffix naming convention, cost_basis variants share a common prefix
let names = vec!["cost_basis_max", "cost_basis_min", "cost_basis"];
assert_eq!(find_common_prefix(&names), Some("cost_basis_".to_string()));
}
#[test]
fn test_common_suffix_none() {
let names = vec!["foo", "bar", "baz"];
assert_eq!(find_common_suffix(&names), None);
}
#[test]
fn test_common_prefix_one_is_prefix_of_other() {
// When one name is a prefix of another (block_count vs block_count_cumulative)
let names = vec!["block_count_cumulative", "block_count"];
assert_eq!(find_common_prefix(&names), Some("block_count_".to_string()));
}
#[test]
fn test_common_suffix_realized_loss() {
let names = vec![
"cumulative_realized_loss",
"net_realized_loss",
"realized_loss",
];
assert_eq!(
find_common_suffix(&names),
Some("_realized_loss".to_string())
);
}
}
+349
View File
@@ -0,0 +1,349 @@
//! Structural pattern detection using bottom-up analysis.
//!
//! This module detects repeating tree structures and analyzes them
//! using the bottom-up name deconstruction algorithm.
use std::collections::{BTreeSet, HashMap};
use brk_types::{TreeNode, extract_json_type};
use super::analyze_pattern_modes;
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>,
/// Counts how many times each signature appears
signature_counts: HashMap<Vec<PatternField>, usize>,
/// Maps normalized signatures to pattern names (for naming consistency)
normalized_to_name: HashMap<Vec<PatternField>, String>,
/// Counts pattern name usage (for unique naming)
name_counts: HashMap<String, usize>,
/// Maps signatures to their child field lists
signature_to_child_fields: HashMap<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(),
}
}
}
/// Detect structural patterns in the tree using a bottom-up approach.
///
/// Returns (patterns, concrete_to_pattern, concrete_to_type_param, node_bases).
/// Each pattern has its `mode` set based on analysis of all instances.
/// `node_bases` maps tree paths to their computed PatternBaseResult for use during generation.
pub fn detect_structural_patterns(
tree: &TreeNode,
) -> (
Vec<StructuralPattern>,
HashMap<Vec<PatternField>, String>,
HashMap<Vec<PatternField>, String>,
HashMap<String, PatternBaseResult>,
) {
let mut ctx = PatternContext::new();
resolve_branch_patterns(tree, "root", &mut ctx);
let (generic_patterns, generic_mappings, type_mappings) =
detect_generic_patterns(&ctx.signature_to_pattern);
// Only include patterns that appear 2+ times for the patterns list
let mut patterns: Vec<StructuralPattern> = ctx
.signature_to_pattern
.iter()
.filter(|(sig, _)| {
ctx.signature_counts.get(*sig).copied().unwrap_or(0) >= 2
&& !generic_mappings.contains_key(*sig)
})
.map(|(fields, name)| {
let child_fields_list = ctx.signature_to_child_fields.get(fields);
let fields_with_type_params = fields
.iter()
.enumerate()
.map(|(i, f)| {
let type_param = child_fields_list
.and_then(|list| list.get(i))
.and_then(|cf| type_mappings.get(cf).cloned());
PatternField {
type_param,
..f.clone()
}
})
.collect();
StructuralPattern {
name: name.clone(),
fields: fields_with_type_params,
mode: None, // Will be determined by analyze_pattern_modes
is_generic: false,
}
})
.collect();
patterns.extend(generic_patterns);
// Build pattern lookup for mode analysis (patterns appearing 2+ times)
let mut pattern_lookup: HashMap<Vec<PatternField>, String> = HashMap::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());
}
}
pattern_lookup.extend(generic_mappings.clone());
let concrete_to_pattern = pattern_lookup.clone();
// Analyze pattern modes (suffix vs prefix) from all instances
// 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, 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>,
) -> (
Vec<StructuralPattern>,
HashMap<Vec<PatternField>, String>,
HashMap<Vec<PatternField>, String>,
) {
let mut normalized_groups: HashMap<
Vec<PatternField>,
Vec<(Vec<PatternField>, String, String)>,
> = HashMap::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));
}
}
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();
for (normalized_fields, group) in normalized_groups {
if group.len() >= 2 {
let generic_name = group[0].1.clone();
for (concrete_fields, _, extracted_type) in &group {
pattern_mappings.insert(concrete_fields.clone(), generic_name.clone());
type_mappings.insert(concrete_fields.clone(), extracted_type.clone());
}
patterns.push(StructuralPattern {
name: generic_name,
fields: normalized_fields,
mode: None, // Will be determined by analyze_pattern_modes
is_generic: true,
});
}
}
(patterns, pattern_mappings, type_mappings)
}
/// Normalize fields by replacing concrete value types with "T".
///
/// Handles two cases:
/// 1. All leaves have identical types (e.g., all `Sats`) -> normalize to `T`
/// 2. All leaves have wrapper types with the same inner type (e.g., `Open<Sats>`, `High<Sats>`)
/// -> normalize to `Open<T>`, `High<T>`, etc.
fn normalize_fields_for_generic(fields: &[PatternField]) -> Option<(Vec<PatternField>, String)> {
let leaf_types: Vec<&str> = fields
.iter()
.filter(|f| f.is_leaf())
.map(|f| f.rust_type.as_str())
.collect();
if leaf_types.is_empty() {
return None;
}
let first_type = leaf_types[0];
// Case 1: All leaf types are identical
if leaf_types.iter().all(|t| *t == first_type) {
let normalized = fields
.iter()
.map(|f| {
if f.is_branch() {
f.clone()
} else {
PatternField {
name: f.name.clone(),
rust_type: "T".to_string(),
json_type: "T".to_string(),
indexes: f.indexes.clone(),
type_param: None,
}
}
})
.collect();
return Some((normalized, crate::extract_inner_type(first_type)));
}
// Case 2: Check if all leaves have wrapper types with the same inner type
// e.g., Open<Sats>, High<Sats>, Low<Sats>, Close<Sats> all have inner type Sats
let inner_types: Vec<String> = leaf_types
.iter()
.map(|t| crate::extract_inner_type(t))
.collect();
let first_inner = &inner_types[0];
// 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)
{
let normalized = fields
.iter()
.map(|f| {
if f.is_branch() {
f.clone()
} else {
PatternField {
name: f.name.clone(),
rust_type: replace_inner_type(&f.rust_type, "T"),
json_type: replace_inner_type(&f.json_type, "T"),
indexes: f.indexes.clone(),
type_param: None,
}
}
})
.collect();
return Some((normalized, first_inner.clone()));
}
None
}
/// Replace the inner type of a wrapper generic with a new type.
/// e.g., `Open<Sats>` with replacement `T` -> `Open<T>`
fn replace_inner_type(type_str: &str, replacement: &str) -> String {
if let Some(start) = type_str.find('<')
&& let Some(end) = type_str.rfind('>')
&& start < end
{
format!("{}<{}>", &type_str[..start], replacement)
} else {
replacement.to_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;
};
let mut fields: Vec<PatternField> = Vec::new();
let mut child_fields_vec: Vec<Vec<PatternField>> = Vec::new();
for (child_name, child_node) in children {
let (rust_type, json_type, indexes, child_fields) = match child_node {
TreeNode::Leaf(leaf) => (
leaf.kind().to_string(),
extract_json_type(&leaf.schema),
leaf.indexes().clone(),
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()));
(
pattern_name.clone(),
pattern_name,
BTreeSet::new(),
child_pattern_fields,
)
}
};
fields.push(PatternField {
name: child_name.clone(),
rust_type,
json_type,
indexes,
type_param: None,
});
child_fields_vec.push(child_fields);
}
fields.sort_by(|a, b| a.name.cmp(&b.name));
*ctx.signature_counts.entry(fields.clone()).or_insert(0) += 1;
ctx.signature_to_child_fields
.entry(fields.clone())
.or_insert(child_fields_vec);
let pattern_name = if let Some(existing) = ctx.signature_to_pattern.get(&fields) {
existing.clone()
} else {
let normalized = normalize_fields_for_naming(&fields);
let name = ctx
.normalized_to_name
.entry(normalized)
.or_insert_with(|| generate_pattern_name(field_name, &mut ctx.name_counts))
.clone();
ctx.signature_to_pattern.insert(fields.clone(), name.clone());
name
};
Some((pattern_name, fields))
}
/// Normalize fields for naming (same structure = same name).
fn normalize_fields_for_naming(fields: &[PatternField]) -> Vec<PatternField> {
fields
.iter()
.map(|f| {
if f.is_branch() {
f.clone()
} else {
PatternField {
name: f.name.clone(),
rust_type: "_".to_string(),
json_type: "_".to_string(),
indexes: f.indexes.clone(),
type_param: None,
}
}
})
.collect()
}
/// Generate a unique pattern name.
fn generate_pattern_name(field_name: &str, name_counts: &mut HashMap<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)
} else {
pascal
};
let base_name = format!("{}Pattern", sanitized);
let count = name_counts.entry(base_name.clone()).or_insert(0);
*count += 1;
if *count == 1 {
base_name
} else {
format!("{}{}", base_name, count)
}
}
@@ -0,0 +1,468 @@
//! Pattern mode detection and field part extraction.
//!
//! This module analyzes pattern instances to detect whether they use
//! suffix mode (fields append to acc) or prefix mode (fields prepend to acc),
//! and extracts the field parts (relatives or prefixes) for code generation.
use std::collections::HashMap;
use brk_types::TreeNode;
use super::{find_common_prefix, find_common_suffix, get_node_fields, normalize_prefix};
use crate::{PatternBaseResult, PatternField, PatternMode, StructuralPattern, build_child_path};
/// Result of analyzing a single pattern instance.
#[derive(Debug, Clone)]
struct InstanceAnalysis {
/// The base to return to parent (used for nesting)
base: String,
/// For suffix mode: field -> relative name
/// For prefix mode: field -> prefix
field_parts: HashMap<String, String>,
/// Whether this instance appears to be suffix mode
is_suffix_mode: bool,
}
/// Analyze all pattern instances and determine their modes.
///
/// This is the main entry point for mode detection. It processes
/// the tree bottom-up, collecting analysis for each pattern instance,
/// then determines the consistent mode for each pattern.
///
/// Returns a map from tree paths to their computed PatternBaseResult.
/// This map is used during generation to check pattern compatibility.
pub fn analyze_pattern_modes(
tree: &TreeNode,
patterns: &mut [StructuralPattern],
pattern_lookup: &HashMap<Vec<PatternField>, String>,
) -> HashMap<String, PatternBaseResult> {
// Collect analyses from all instances, keyed by pattern name
let mut all_analyses: HashMap<String, Vec<InstanceAnalysis>> = HashMap::new();
// Also collect base results for each node, keyed by tree path
let mut node_bases: HashMap<String, PatternBaseResult> = HashMap::new();
// Bottom-up traversal
collect_instance_analyses(tree, "", pattern_lookup, &mut all_analyses, &mut node_bases);
// For each pattern, determine mode from collected instances
for pattern in patterns.iter_mut() {
if let Some(analyses) = all_analyses.get(&pattern.name) {
pattern.mode = determine_pattern_mode(analyses, &pattern.fields);
}
}
node_bases
}
/// Recursively collect instance analyses bottom-up.
/// Returns the "base" for this node (used by parent for its analysis).
///
/// Also stores the PatternBaseResult for each node in `node_bases`, keyed by path.
fn collect_instance_analyses(
node: &TreeNode,
path: &str,
pattern_lookup: &HashMap<Vec<PatternField>, String>,
all_analyses: &mut HashMap<String, Vec<InstanceAnalysis>>,
node_bases: &mut HashMap<String, PatternBaseResult>,
) -> Option<String> {
match node {
TreeNode::Leaf(leaf) => {
// Leaves return their metric name as the base
Some(leaf.name().to_string())
}
TreeNode::Branch(children) => {
// First, process all children recursively (bottom-up)
let mut child_bases: HashMap<String, String> = HashMap::new();
for (field_name, child_node) in children {
let child_path = build_child_path(path, field_name);
if let Some(base) = collect_instance_analyses(
child_node,
&child_path,
pattern_lookup,
all_analyses,
node_bases,
) {
child_bases.insert(field_name.clone(), base);
}
}
if child_bases.is_empty() {
return None;
}
// Analyze this instance
let analysis = analyze_instance(&child_bases);
// Store the base result for this node
// Note: has_outlier is false because we use recursive base computation
// which gives correct bases without needing outlier detection
node_bases.insert(
path.to_string(),
PatternBaseResult {
base: analysis.base.clone(),
has_outlier: false,
is_suffix_mode: analysis.is_suffix_mode,
field_parts: analysis.field_parts.clone(),
},
);
// Get the pattern name for this node (if any)
let fields = get_node_fields(children, pattern_lookup);
if let Some(pattern_name) = pattern_lookup.get(&fields) {
all_analyses
.entry(pattern_name.clone())
.or_default()
.push(analysis.clone());
}
// Return the base for parent
Some(analysis.base)
}
}
}
/// Analyze a single pattern instance from its child bases.
fn analyze_instance(child_bases: &HashMap<String, String>) -> InstanceAnalysis {
let bases: Vec<&str> = child_bases.values().map(|s| s.as_str()).collect();
// Try suffix mode first: look for common prefix among children
if let Some(common_prefix) = find_common_prefix(&bases) {
let base = common_prefix.trim_end_matches('_').to_string();
let mut field_parts = HashMap::new();
for (field_name, child_base) in child_bases {
// Relative = child_base with common prefix stripped
// If child_base equals base, relative is empty (identity field)
let relative = if child_base == &base {
String::new()
} else {
child_base
.strip_prefix(&common_prefix)
.unwrap_or(child_base)
.to_string()
};
field_parts.insert(field_name.clone(), relative);
}
return InstanceAnalysis {
base,
field_parts,
is_suffix_mode: true,
};
}
// Try prefix mode: look for common suffix among children
if let Some(common_suffix) = find_common_suffix(&bases) {
let base = common_suffix.trim_start_matches('_').to_string();
let mut field_parts = HashMap::new();
for (field_name, child_base) in child_bases {
// Prefix = child_base with common suffix stripped, normalized to end with _
let prefix = child_base
.strip_suffix(&common_suffix)
.map(normalize_prefix)
.unwrap_or_default();
field_parts.insert(field_name.clone(), prefix);
}
return InstanceAnalysis {
base,
field_parts,
is_suffix_mode: false,
};
}
// No common prefix or suffix - use empty base so _m(base, relative) returns just the relative.
// This handles cases like utxo_cohorts.all.activity where children have completely
// different bases (coinblocks_destroyed, coindays_destroyed, etc.)
let field_parts = child_bases
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
InstanceAnalysis {
base: String::new(),
field_parts,
is_suffix_mode: true,
}
}
/// Determine the consistent mode for a pattern from all its instances.
/// Uses majority voting: if most instances agree on mode and field_parts,
/// use those. Minority instances will be inlined at usage sites.
fn determine_pattern_mode(
analyses: &[InstanceAnalysis],
fields: &[PatternField],
) -> Option<PatternMode> {
if analyses.is_empty() {
return None;
}
// Group instances by (mode, field_parts) signature
let suffix_instances: Vec<_> = analyses.iter().filter(|a| a.is_suffix_mode).collect();
let prefix_instances: Vec<_> = analyses.iter().filter(|a| !a.is_suffix_mode).collect();
// Pick the majority mode group
let (majority_instances, is_suffix) = if suffix_instances.len() >= prefix_instances.len() {
(suffix_instances, true)
} else {
(prefix_instances, false)
};
if majority_instances.is_empty() {
return None;
}
// Find the most common field_parts within the majority group
// Convert to sorted Vec for comparison since HashMap isn't hashable
let mut parts_counts: HashMap<Vec<(String, String)>, usize> = HashMap::new();
for analysis in &majority_instances {
let mut sorted: Vec<_> = analysis
.field_parts
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
sorted.sort();
*parts_counts.entry(sorted).or_insert(0) += 1;
}
let (best_parts_vec, _count) = parts_counts.into_iter().max_by_key(|(_, count)| *count)?;
let best_parts: HashMap<String, String> = best_parts_vec.into_iter().collect();
// Verify all required fields have parts
for field in fields {
if !best_parts.contains_key(&field.name) {
return None;
}
}
let field_parts = best_parts;
if is_suffix {
Some(PatternMode::Suffix {
relatives: field_parts,
})
} else {
Some(PatternMode::Prefix {
prefixes: field_parts,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_analyze_instance_suffix_mode() {
let mut child_bases = HashMap::new();
child_bases.insert("max".to_string(), "lth_cost_basis_max".to_string());
child_bases.insert("min".to_string(), "lth_cost_basis_min".to_string());
child_bases.insert("percentiles".to_string(), "lth_cost_basis".to_string());
let analysis = analyze_instance(&child_bases);
assert!(analysis.is_suffix_mode);
assert_eq!(analysis.base, "lth_cost_basis");
assert_eq!(analysis.field_parts.get("max"), Some(&"max".to_string()));
assert_eq!(analysis.field_parts.get("min"), Some(&"min".to_string()));
assert_eq!(
analysis.field_parts.get("percentiles"),
Some(&"".to_string())
);
}
#[test]
fn test_analyze_instance_prefix_mode() {
// Period-prefixed metrics like "1y_lump_sum_stack", "1m_lump_sum_stack"
// share a common suffix "_lump_sum_stack" with different period prefixes
let mut child_bases = HashMap::new();
child_bases.insert("_1y".to_string(), "1y_lump_sum_stack".to_string());
child_bases.insert("_1m".to_string(), "1m_lump_sum_stack".to_string());
child_bases.insert("_1w".to_string(), "1w_lump_sum_stack".to_string());
let analysis = analyze_instance(&child_bases);
assert!(!analysis.is_suffix_mode);
assert_eq!(analysis.base, "lump_sum_stack");
assert_eq!(analysis.field_parts.get("_1y"), Some(&"1y_".to_string()));
assert_eq!(analysis.field_parts.get("_1m"), Some(&"1m_".to_string()));
assert_eq!(analysis.field_parts.get("_1w"), Some(&"1w_".to_string()));
}
#[test]
fn test_analyze_instance_root_suffix() {
// At root level with suffix naming convention
let mut child_bases = HashMap::new();
child_bases.insert("max".to_string(), "cost_basis_max".to_string());
child_bases.insert("min".to_string(), "cost_basis_min".to_string());
child_bases.insert("percentiles".to_string(), "cost_basis".to_string());
let analysis = analyze_instance(&child_bases);
// With suffix naming, common prefix is "cost_basis_" (since cost_basis is one of the names)
assert!(analysis.is_suffix_mode);
assert_eq!(analysis.base, "cost_basis");
assert_eq!(analysis.field_parts.get("max"), Some(&"max".to_string()));
assert_eq!(analysis.field_parts.get("min"), Some(&"min".to_string()));
assert_eq!(
analysis.field_parts.get("percentiles"),
Some(&"".to_string())
);
}
#[test]
fn test_determine_pattern_mode_majority_voting() {
// Test that majority voting works when instances have mixed modes.
// This simulates CostBasisPattern2: most instances use suffix mode,
// but root-level uses prefix mode (max_cost_basis, min_cost_basis, cost_basis).
use std::collections::BTreeSet;
let fields = vec![
PatternField {
name: "max".to_string(),
rust_type: "TestType".to_string(),
json_type: "number".to_string(),
indexes: BTreeSet::new(),
type_param: None,
},
PatternField {
name: "min".to_string(),
rust_type: "TestType".to_string(),
json_type: "number".to_string(),
indexes: BTreeSet::new(),
type_param: None,
},
PatternField {
name: "percentiles".to_string(),
rust_type: "TestType".to_string(),
json_type: "number".to_string(),
indexes: BTreeSet::new(),
type_param: None,
},
];
// 3 suffix mode instances (majority)
let suffix1 = InstanceAnalysis {
base: "lth_cost_basis".to_string(),
field_parts: [
("max".to_string(), "max".to_string()),
("min".to_string(), "min".to_string()),
("percentiles".to_string(), "".to_string()),
]
.into_iter()
.collect(),
is_suffix_mode: true,
};
let suffix2 = InstanceAnalysis {
base: "sth_cost_basis".to_string(),
field_parts: [
("max".to_string(), "max".to_string()),
("min".to_string(), "min".to_string()),
("percentiles".to_string(), "".to_string()),
]
.into_iter()
.collect(),
is_suffix_mode: true,
};
let suffix3 = InstanceAnalysis {
base: "utxo_cost_basis".to_string(),
field_parts: [
("max".to_string(), "max".to_string()),
("min".to_string(), "min".to_string()),
("percentiles".to_string(), "".to_string()),
]
.into_iter()
.collect(),
is_suffix_mode: true,
};
// 1 prefix mode instance (minority - root level)
let prefix1 = InstanceAnalysis {
base: "cost_basis".to_string(),
field_parts: [
("max".to_string(), "max_".to_string()),
("min".to_string(), "min_".to_string()),
("percentiles".to_string(), "".to_string()),
]
.into_iter()
.collect(),
is_suffix_mode: false,
};
let analyses = vec![suffix1, suffix2, suffix3, prefix1];
let mode = determine_pattern_mode(&analyses, &fields);
// Should pick suffix mode (majority) with the common field_parts
assert!(mode.is_some());
match mode.unwrap() {
PatternMode::Suffix { relatives } => {
assert_eq!(relatives.get("max"), Some(&"max".to_string()));
assert_eq!(relatives.get("min"), Some(&"min".to_string()));
assert_eq!(relatives.get("percentiles"), Some(&"".to_string()));
}
PatternMode::Prefix { .. } => {
panic!("Expected suffix mode, got prefix mode");
}
}
}
#[test]
fn test_determine_pattern_mode_all_same() {
// Test when all instances agree on mode and field_parts
use std::collections::BTreeSet;
let fields = vec![
PatternField {
name: "max".to_string(),
rust_type: "TestType".to_string(),
json_type: "number".to_string(),
indexes: BTreeSet::new(),
type_param: None,
},
PatternField {
name: "min".to_string(),
rust_type: "TestType".to_string(),
json_type: "number".to_string(),
indexes: BTreeSet::new(),
type_param: None,
},
];
let instance1 = InstanceAnalysis {
base: "metric_a".to_string(),
field_parts: [
("max".to_string(), "max".to_string()),
("min".to_string(), "min".to_string()),
]
.into_iter()
.collect(),
is_suffix_mode: true,
};
let instance2 = InstanceAnalysis {
base: "metric_b".to_string(),
field_parts: [
("max".to_string(), "max".to_string()),
("min".to_string(), "min".to_string()),
]
.into_iter()
.collect(),
is_suffix_mode: true,
};
let analyses = vec![instance1, instance2];
let mode = determine_pattern_mode(&analyses, &fields);
assert!(mode.is_some());
match mode.unwrap() {
PatternMode::Suffix { relatives } => {
assert_eq!(relatives.get("max"), Some(&"max".to_string()));
assert_eq!(relatives.get("min"), Some(&"min".to_string()));
}
PatternMode::Prefix { .. } => {
panic!("Expected suffix mode");
}
}
}
}
+571
View File
@@ -0,0 +1,571 @@
//! Tree traversal helpers for pattern analysis.
//!
//! 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 brk_types::{Index, TreeNode, extract_json_type};
use crate::{IndexSetPattern, PatternField, child_type_name};
use super::{find_common_prefix, find_common_suffix, normalize_prefix};
/// Get the shortest leaf name from a tree node.
///
/// 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> {
match node {
TreeNode::Leaf(leaf) => Some(leaf.name().to_string()),
TreeNode::Branch(children) => children
.values()
.filter_map(get_shortest_leaf_name)
.min_by_key(|name| name.len()),
}
}
/// Get the field signature for a branch node's children.
pub fn get_node_fields(
children: &BTreeMap<String, TreeNode>,
pattern_lookup: &HashMap<Vec<PatternField>, String>,
) -> Vec<PatternField> {
let mut fields: Vec<PatternField> = children
.iter()
.map(|(name, node)| {
let (rust_type, json_type, indexes) = match node {
TreeNode::Leaf(leaf) => (
leaf.kind().to_string(),
extract_json_type(&leaf.schema),
leaf.indexes().clone(),
),
TreeNode::Branch(grandchildren) => {
let child_fields = get_node_fields(grandchildren, pattern_lookup);
let pattern_name = pattern_lookup
.get(&child_fields)
.cloned()
.unwrap_or_else(|| "Unknown".to_string());
(pattern_name.clone(), pattern_name, BTreeSet::new())
}
};
PatternField {
name: name.clone(),
rust_type,
json_type,
indexes,
type_param: None,
}
})
.collect();
fields.sort_by(|a, b| a.name.cmp(&b.name));
fields
}
/// Detect index patterns (sets of indexes that appear together on metrics).
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);
// Sort by count (descending) then by first index name for deterministic ordering
let mut sorted_sets: Vec<_> = unique_index_sets
.into_iter()
.filter(|indexes| !indexes.is_empty())
.collect();
sorted_sets.sort_by(|a, b| {
b.len()
.cmp(&a.len())
.then_with(|| a.iter().next().cmp(&b.iter().next()))
});
// Assign unique sequential names
sorted_sets
.into_iter()
.enumerate()
.map(|(i, indexes)| IndexSetPattern {
name: format!("MetricPattern{}", i + 1),
indexes,
})
.collect()
}
fn collect_index_sets_from_tree(
node: &TreeNode,
unique_index_sets: &mut BTreeSet<BTreeSet<Index>>,
) {
match node {
TreeNode::Leaf(leaf) => {
unique_index_sets.insert(leaf.indexes().clone());
}
TreeNode::Branch(children) => {
for child in children.values() {
collect_index_sets_from_tree(child, unique_index_sets);
}
}
}
}
/// Result of analyzing a pattern instance's base.
#[derive(Debug, Clone)]
pub struct PatternBaseResult {
/// The computed base name for the pattern.
pub base: String,
/// Whether an outlier child was excluded to find the pattern.
/// If true, pattern factory should not be used.
pub has_outlier: bool,
/// Whether this instance uses suffix mode (common prefix) or prefix mode (common suffix).
/// Used to check compatibility with the pattern's mode.
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>,
}
impl PatternBaseResult {
/// Create a default result that forces inlining (has_outlier = true).
/// Use when no pattern base could be computed during lookup.
pub fn force_inline() -> Self {
Self {
base: String::new(),
has_outlier: true,
is_suffix_mode: true,
field_parts: HashMap::new(),
}
}
/// Create an empty result with no outlier.
/// Use for root-level patterns or when children have no common pattern.
pub fn empty() -> Self {
Self {
base: String::new(),
has_outlier: false,
is_suffix_mode: true,
field_parts: HashMap::new(),
}
}
}
/// Get the metric base for a pattern instance by analyzing direct children.
///
/// Uses the shortest leaf names from direct children to find common prefix/suffix.
///
/// If the initial analysis fails to find a common pattern, it tries excluding
/// each child one at a time to detect outliers (e.g., a mismatched "base" field
/// from indexer/computed tree merging).
///
/// Returns both the base and whether an outlier was detected.
pub fn get_pattern_instance_base(node: &TreeNode) -> PatternBaseResult {
let child_names = get_direct_children_for_analysis(node);
if child_names.is_empty() {
return PatternBaseResult::empty();
}
// Try to find common base from leaf names
if let Some(result) = try_find_base(&child_names, false) {
return PatternBaseResult {
base: result.base,
has_outlier: result.has_outlier,
is_suffix_mode: result.is_suffix_mode,
field_parts: result.field_parts,
};
}
// If no common pattern found and we have enough children, try excluding outliers
if child_names.len() > 2 {
for i in 0..child_names.len() {
let filtered: Vec<_> = child_names
.iter()
.enumerate()
.filter(|(j, _)| *j != i)
.map(|(_, v)| v.clone())
.collect();
if let Some(result) = try_find_base(&filtered, true) {
return PatternBaseResult {
base: result.base,
has_outlier: true,
is_suffix_mode: result.is_suffix_mode,
field_parts: result.field_parts,
};
}
}
}
// Fallback: no common prefix/suffix found - this is a root-level pattern
// Return empty base so metric names are used directly
PatternBaseResult::empty()
}
/// Result of try_find_base: base name, has_outlier flag, is_suffix_mode flag, and field_parts.
struct FindBaseResult {
base: String,
has_outlier: bool,
is_suffix_mode: bool,
field_parts: HashMap<String, String>,
}
/// Try to find a common base from child names using prefix/suffix detection.
/// Returns Some(FindBaseResult) if found.
fn try_find_base(
child_names: &[(String, String)],
is_outlier_attempt: bool,
) -> Option<FindBaseResult> {
let leaf_names: Vec<&str> = child_names.iter().map(|(_, n)| n.as_str()).collect();
// 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();
for (field_name, leaf_name) in child_names {
// Compute the suffix part for this field
let suffix = if leaf_name == &base {
String::new()
} else {
leaf_name
.strip_prefix(&prefix)
.unwrap_or(leaf_name)
.to_string()
};
field_parts.insert(field_name.clone(), suffix);
}
return Some(FindBaseResult {
base,
has_outlier: is_outlier_attempt,
is_suffix_mode: true,
field_parts,
});
}
// 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();
for (field_name, leaf_name) in child_names {
// Compute the prefix part for this field, normalized to end with _
let prefix_part = leaf_name
.strip_suffix(&suffix)
.map(normalize_prefix)
.unwrap_or_default();
field_parts.insert(field_name.clone(), prefix_part);
}
return Some(FindBaseResult {
base,
has_outlier: is_outlier_attempt,
is_suffix_mode: false,
field_parts,
});
}
None
}
/// Get (field_name, shortest_leaf_name) pairs for direct children of a branch node.
///
/// Uses the shortest leaf name from each child subtree to find the "base" case
/// (the leaf without suffix modifiers like `_btc` or `_usd`).
fn get_direct_children_for_analysis(node: &TreeNode) -> Vec<(String, String)> {
match node {
TreeNode::Leaf(leaf) => vec![(leaf.name().to_string(), leaf.name().to_string())],
TreeNode::Branch(children) => children
.iter()
.filter_map(|(field_name, child)| {
get_shortest_leaf_name(child).map(|leaf_name| (field_name.clone(), leaf_name))
})
.collect(),
}
}
/// Infer the accumulated name for a child node based on a descendant leaf name.
pub fn infer_accumulated_name(parent_acc: &str, field_name: &str, descendant_leaf: &str) -> String {
if let Some(pos) = descendant_leaf.find(field_name) {
if pos == 0 {
return field_name.to_string();
}
if pos > 0 && descendant_leaf.chars().nth(pos - 1) == Some('_') {
return if parent_acc.is_empty() {
field_name.to_string()
} else {
format!("{}_{}", parent_acc, field_name)
};
}
}
if parent_acc.is_empty() {
field_name.to_string()
} else {
format!("{}_{}", parent_acc, field_name)
}
}
/// Get fields with child field information for generic pattern lookup.
pub fn get_fields_with_child_info(
children: &BTreeMap<String, TreeNode>,
parent_name: &str,
pattern_lookup: &HashMap<Vec<PatternField>, String>,
) -> Vec<(PatternField, Option<Vec<PatternField>>)> {
children
.iter()
.map(|(name, node)| {
let (rust_type, json_type, indexes, child_fields) = match node {
TreeNode::Leaf(leaf) => (
leaf.kind().to_string(),
extract_json_type(&leaf.schema),
leaf.indexes().clone(),
None,
),
TreeNode::Branch(grandchildren) => {
let child_fields = get_node_fields(grandchildren, pattern_lookup);
let pattern_name = pattern_lookup
.get(&child_fields)
.cloned()
.unwrap_or_else(|| child_type_name(parent_name, name));
(
pattern_name.clone(),
pattern_name,
BTreeSet::new(),
Some(child_fields),
)
}
};
(
PatternField {
name: name.clone(),
rust_type,
json_type,
indexes,
type_param: None,
},
child_fields,
)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use brk_types::{MetricLeaf, MetricLeafWithSchema, TreeNode};
use std::collections::BTreeMap;
fn make_leaf(name: &str) -> TreeNode {
let leaf = MetricLeaf {
name: name.to_string(),
kind: "TestType".to_string(),
indexes: BTreeSet::new(),
};
TreeNode::Leaf(MetricLeafWithSchema::new(leaf, serde_json::json!({})))
}
fn make_branch(children: Vec<(&str, TreeNode)>) -> TreeNode {
let map: BTreeMap<String, TreeNode> = children
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
TreeNode::Branch(map)
}
#[test]
fn test_get_pattern_instance_base_with_base_field() {
// Simulates vbytes tree: has base field with block_vbytes leaf
let tree = make_branch(vec![
(
"base",
make_branch(vec![("dateindex", make_leaf("block_vbytes"))]),
),
(
"average",
make_branch(vec![("dateindex", make_leaf("block_vbytes_average"))]),
),
(
"sum",
make_branch(vec![("dateindex", make_leaf("block_vbytes_sum"))]),
),
]);
let result = get_pattern_instance_base(&tree);
assert_eq!(result.base, "block_vbytes");
assert!(!result.has_outlier);
}
#[test]
fn test_get_pattern_instance_base_without_base_field() {
// Simulates weight tree: NO base field, only suffixed metrics
let tree = make_branch(vec![
(
"average",
make_branch(vec![("dateindex", make_leaf("block_weight_average"))]),
),
(
"sum",
make_branch(vec![("dateindex", make_leaf("block_weight_sum"))]),
),
(
"cumulative",
make_branch(vec![("dateindex", make_leaf("block_weight_cumulative"))]),
),
(
"max",
make_branch(vec![("dateindex", make_leaf("block_weight_max"))]),
),
(
"min",
make_branch(vec![("dateindex", make_leaf("block_weight_min"))]),
),
]);
let result = get_pattern_instance_base(&tree);
assert_eq!(result.base, "block_weight");
assert!(!result.has_outlier);
}
#[test]
fn test_get_pattern_instance_base_with_duplicate_base_field() {
// What if there's a "base" field that points to the same leaf as "average"?
// This could happen if the tree generation creates a base field that shares leaves with average
let tree = make_branch(vec![
(
"base",
make_branch(vec![("dateindex", make_leaf("block_weight_average"))]),
),
(
"average",
make_branch(vec![("dateindex", make_leaf("block_weight_average"))]),
),
(
"sum",
make_branch(vec![("dateindex", make_leaf("block_weight_sum"))]),
),
]);
let result = get_pattern_instance_base(&tree);
// Common prefix among all children is "block_weight_"
assert_eq!(result.base, "block_weight");
assert!(!result.has_outlier);
}
#[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.
// 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
("average", make_leaf("block_weight_average")),
("sum", make_leaf("block_weight_sum")),
("cumulative", make_leaf("block_weight_cumulative")),
("max", make_leaf("block_weight_max")),
("min", make_leaf("block_weight_min")),
]);
let result = get_pattern_instance_base(&tree);
// Should detect "weight" as outlier and find common prefix from others
assert_eq!(result.base, "block_weight");
assert!(result.has_outlier); // Pattern factory should NOT be used
}
#[test]
fn test_get_pattern_instance_base_root_level_no_common_pattern() {
// Simulates root-level pattern with metrics 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.
let tree = make_branch(vec![
("alpha", make_leaf("foo_metric")),
("beta", make_leaf("bar_value")),
("gamma", make_leaf("baz_count")),
]);
let result = get_pattern_instance_base(&tree);
// No common prefix or suffix - return empty base
assert_eq!(result.base, "");
assert!(!result.has_outlier);
}
#[test]
fn test_get_pattern_instance_base_two_children_no_pattern() {
// Two children with no common pattern - should still return empty base
let tree = make_branch(vec![
("foo", make_leaf("alpha")),
("bar", make_leaf("beta")),
]);
let result = get_pattern_instance_base(&tree);
assert_eq!(result.base, "");
assert!(!result.has_outlier);
}
#[test]
fn test_get_pattern_instance_base_with_outlier_excluded() {
// Simulates the realized pattern: adjusted_sopr, sopr, asopr.
// When "asopr" is excluded as outlier, "adjusted_sopr" and "sopr" share suffix "_sopr".
// The outlier detection should find base="sopr" with has_outlier=true.
let tree = make_branch(vec![
("adjustedSopr", make_leaf("adjusted_sopr")),
("sopr", make_leaf("sopr")),
("asopr", make_leaf("asopr")),
]);
let result = get_pattern_instance_base(&tree);
// Outlier detected - pattern base found by excluding "asopr"
assert_eq!(result.base, "sopr");
assert!(result.has_outlier); // Pattern factory should NOT be used (inline instead)
}
#[test]
fn test_get_pattern_instance_base_suffix_mode_price_ago() {
// Simulates price_ago pattern: price_1d_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")),
("_1w", make_leaf("price_1w_ago")),
("_1m", make_leaf("price_1m_ago")),
("_10y", make_leaf("price_10y_ago")),
]);
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.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
// Common suffix is "_price_returns", so this is prefix mode
let tree = make_branch(vec![
("_1d", make_leaf("1d_price_returns")),
("_1w", make_leaf("1w_price_returns")),
("_1m", make_leaf("1m_price_returns")),
("_10y", make_leaf("10y_price_returns")),
]);
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.has_outlier);
}
#[test]
fn test_mode_detection_distinguishes_similar_structures() {
// Two patterns with identical structure but different naming conventions
// should have different modes detected
// Suffix mode pattern
let suffix_tree = make_branch(vec![
("_1y", make_leaf("lump_sum_1y")),
("_2y", make_leaf("lump_sum_2y")),
("_5y", make_leaf("lump_sum_5y")),
]);
let suffix_result = get_pattern_instance_base(&suffix_tree);
assert_eq!(suffix_result.base, "lump_sum");
assert!(suffix_result.is_suffix_mode);
// Prefix mode pattern (same structure, different naming)
let prefix_tree = make_branch(vec![
("_1y", make_leaf("1y_returns")),
("_2y", make_leaf("2y_returns")),
("_5y", make_leaf("5y_returns")),
]);
let prefix_result = get_pattern_instance_base(&prefix_tree);
assert_eq!(prefix_result.base, "returns");
assert!(!prefix_result.is_suffix_mode);
}
}
@@ -0,0 +1,62 @@
//! JavaScript language syntax implementation.
use crate::{GenericSyntax, LanguageSyntax, to_camel_case};
/// JavaScript-specific code generation syntax.
pub struct JavaScriptSyntax;
impl LanguageSyntax for JavaScriptSyntax {
fn field_name(&self, name: &str) -> String {
to_camel_case(name)
}
fn path_expr(&self, base_var: &str, suffix: &str) -> String {
// Convert base_var to camelCase for JavaScript
let var_name = to_camel_case(base_var);
format!("`${{{}}}{}`", var_name, suffix)
}
fn suffix_expr(&self, acc_var: &str, relative: &str) -> String {
let var_name = to_camel_case(acc_var);
if relative.is_empty() {
// Identity: just return acc
var_name
} else {
// _m(acc, relative) -> acc ? `${acc}_relative` : 'relative'
format!("_m({}, '{}')", var_name, relative)
}
}
fn prefix_expr(&self, prefix: &str, acc_var: &str) -> String {
let var_name = to_camel_case(acc_var);
if prefix.is_empty() {
// Identity: just return acc
var_name
} else {
// _p(prefix, acc) -> acc ? `${prefix}${acc}` : 'prefix_without_underscore'
let prefix_base = prefix.trim_end_matches('_');
format!("_p('{}', {})", prefix_base, var_name)
}
}
fn constructor(&self, type_name: &str, path_expr: &str) -> String {
format!("create{}(client, {})", type_name, path_expr)
}
fn field_init(&self, indent: &str, name: &str, _type_ann: &str, value: &str) -> String {
// JavaScript uses object literal syntax; type is in JSDoc, not in assignment
format!("{}{}: {},", indent, name, value)
}
fn generic_syntax(&self) -> GenericSyntax {
GenericSyntax::JAVASCRIPT
}
fn string_literal(&self, value: &str) -> String {
format!("'{}'", value)
}
fn constructor_name(&self, type_name: &str) -> String {
format!("create{}", type_name)
}
}
+12
View File
@@ -0,0 +1,12 @@
//! Language-specific syntax backends.
//!
//! This module contains implementations of the `LanguageSyntax` trait
//! for each supported target language.
mod javascript;
mod python;
mod rust;
pub use javascript::JavaScriptSyntax;
pub use python::PythonSyntax;
pub use rust::RustSyntax;
+57
View File
@@ -0,0 +1,57 @@
//! Python language syntax implementation.
use crate::{GenericSyntax, LanguageSyntax, escape_python_keyword, to_snake_case};
/// Python-specific code generation syntax.
pub struct PythonSyntax;
impl LanguageSyntax for PythonSyntax {
fn field_name(&self, name: &str) -> String {
escape_python_keyword(&to_snake_case(name))
}
fn path_expr(&self, base_var: &str, suffix: &str) -> String {
format!("f'{{{}}}{}'", base_var, suffix)
}
fn suffix_expr(&self, acc_var: &str, relative: &str) -> String {
if relative.is_empty() {
// Identity: just return acc
acc_var.to_string()
} else {
// _m(acc, relative) -> f'{acc}_{relative}' if acc else 'relative'
format!("_m({}, '{}')", acc_var, relative)
}
}
fn prefix_expr(&self, prefix: &str, acc_var: &str) -> String {
if prefix.is_empty() {
// Identity: just return acc
acc_var.to_string()
} else {
// _p(prefix, acc) -> f'{prefix}{acc}' if acc else 'prefix_base'
let prefix_base = prefix.trim_end_matches('_');
format!("_p('{}', {})", prefix_base, acc_var)
}
}
fn constructor(&self, type_name: &str, path_expr: &str) -> String {
format!("{}(client, {})", type_name, path_expr)
}
fn field_init(&self, indent: &str, name: &str, type_ann: &str, value: &str) -> String {
format!("{}self.{}: {} = {}", indent, name, type_ann, value)
}
fn generic_syntax(&self) -> GenericSyntax {
GenericSyntax::PYTHON
}
fn string_literal(&self, value: &str) -> String {
format!("'{}'", value)
}
fn constructor_name(&self, type_name: &str) -> String {
type_name.to_string()
}
}
+58
View File
@@ -0,0 +1,58 @@
//! Rust language syntax implementation.
use crate::{GenericSyntax, LanguageSyntax, to_snake_case};
/// Rust-specific code generation syntax.
pub struct RustSyntax;
impl LanguageSyntax for RustSyntax {
fn field_name(&self, name: &str) -> String {
to_snake_case(name)
}
fn path_expr(&self, base_var: &str, suffix: &str) -> String {
format!("format!(\"{{{}}}{}\")", base_var, suffix)
}
fn suffix_expr(&self, acc_var: &str, relative: &str) -> String {
if relative.is_empty() {
// Identity: just return acc
format!("{}.clone()", acc_var)
} else {
// _m(&acc, relative) -> if acc.is_empty() { relative } else { format!("{acc}_{relative}") }
format!("_m(&{}, \"{}\")", acc_var, relative)
}
}
fn prefix_expr(&self, prefix: &str, acc_var: &str) -> String {
if prefix.is_empty() {
// Identity: just return acc
format!("{}.clone()", 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)
}
}
fn constructor(&self, type_name: &str, path_expr: &str) -> String {
format!("{}::new(client.clone(), {})", type_name, path_expr)
}
fn field_init(&self, indent: &str, name: &str, _type_ann: &str, value: &str) -> String {
// Rust struct initialization; type is in struct definition, not in init
format!("{}{}: {},", indent, name, value)
}
fn generic_syntax(&self) -> GenericSyntax {
GenericSyntax::RUST
}
fn string_literal(&self, value: &str) -> String {
format!("\"{}\".to_string()", value)
}
fn constructor_name(&self, type_name: &str) -> String {
format!("{}::new", type_name)
}
}
@@ -0,0 +1,87 @@
//! Shared constant generation for static client data.
//!
//! Extracts common logic for generating INDEXES, POOL_ID_TO_POOL_NAME,
//! and cohort name constants across JavaScript and Python clients.
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,
};
use brk_types::{pools, Index, PoolSlug};
use serde::Serialize;
use serde_json::Value;
use crate::{to_camel_case, VERSION};
/// Collected constant data for client generation.
pub struct ClientConstants {
pub version: String,
pub indexes: Vec<&'static str>,
pub pool_map: BTreeMap<PoolSlug, &'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 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();
Self {
version: format!("v{}", VERSION),
indexes,
pool_map,
}
}
}
/// Cohort name constants - shared data definitions.
pub struct CohortConstants;
impl CohortConstants {
/// Get all cohort constants as name-value pairs for iteration.
pub fn all() -> Vec<(&'static str, Value)> {
fn to_value<T: Serialize>(v: &T) -> Value {
serde_json::to_value(v).unwrap()
}
vec![
("TERM_NAMES", to_value(&TERM_NAMES)),
("EPOCH_NAMES", to_value(&EPOCH_NAMES)),
("YEAR_NAMES", to_value(&YEAR_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)),
("AMOUNT_RANGE_NAMES", to_value(&AMOUNT_RANGE_NAMES)),
("GE_AMOUNT_NAMES", to_value(&GE_AMOUNT_NAMES)),
("LT_AMOUNT_NAMES", to_value(&LT_AMOUNT_NAMES)),
]
}
}
/// Convert top-level keys of a JSON object to camelCase.
pub fn camel_case_keys(value: Value) -> Value {
match value {
Value::Object(map) => {
let new_map: serde_json::Map<String, Value> = map
.into_iter()
.map(|(k, v)| (to_camel_case(&k), v))
.collect();
Value::Object(new_map)
}
other => other,
}
}
/// Format a JSON value as a pretty-printed string.
pub fn format_json<T: Serialize>(value: &T) -> String {
serde_json::to_string_pretty(value).unwrap()
}
+181
View File
@@ -0,0 +1,181 @@
//! Shared field generation logic.
//!
//! This module contains the core field generation logic that is shared
//! across all language backends. The `LanguageSyntax` trait is used to
//! abstract over language-specific formatting.
use std::fmt::Write;
use brk_types::MetricLeafWithSchema;
use crate::{ClientMetadata, LanguageSyntax, PatternField, 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()
} else {
format!("_{}", name)
}
}
/// Compute path expression from pattern mode and field part.
fn compute_path_expr<S: LanguageSyntax>(
syntax: &S,
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 {
panic!(
"Field '{}' has no matching pattern or index accessor. All metrics must be indexed.",
field.name
)
}
}
/// Generate a parameterized field using the language syntax.
///
/// This is used for pattern instances where fields use an accumulated
/// metric name that's built up through the tree traversal.
pub fn generate_parameterized_field<S: LanguageSyntax>(
output: &mut String,
syntax: &S,
field: &PatternField,
pattern: &StructuralPattern,
metadata: &ClientMetadata,
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);
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.
///
/// This is used when generating tree nodes where we need to detect the pattern instance
/// base from descendant leaf names.
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>,
) {
let field_name = syntax.field_name(&field.name);
let type_ann = metadata.field_type_annotation(field, false, None, syntax.generic_syntax());
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)
} else {
// All metrics must be indexed
panic!(
"Field '{}' is a leaf with no index accessor. All metrics must be indexed.",
field.name
)
};
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.
///
/// This is the shared implementation for all language backends. It uses
/// `leaf.name()` directly to get the correct metric name, avoiding any
/// path concatenation that could produce incorrect names.
///
/// # Arguments
/// * `output` - The string buffer to write to
/// * `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
/// * `metadata` - Client metadata for looking up index patterns
/// * `indent` - Indentation string
pub fn generate_leaf_field<S: LanguageSyntax>(
output: &mut String,
syntax: &S,
client_expr: &str,
tree_field_name: &str,
leaf: &MetricLeafWithSchema,
metadata: &ClientMetadata,
indent: &str,
) {
let field_name = syntax.field_name(tree_field_name);
let accessor = metadata
.find_index_set_pattern(leaf.indexes())
.unwrap_or_else(|| {
panic!(
"Metric '{}' has no matching index pattern. All metrics 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 value = format!(
"{}({}, {})",
syntax.constructor_name(&accessor.name),
client_expr,
metric_name
);
writeln!(
output,
"{}",
syntax.field_init(indent, &field_name, &type_ann, &value)
)
.unwrap();
}
+13
View File
@@ -0,0 +1,13 @@
//! Shared code generation logic.
//!
//! This module contains generation functions that are parameterized by
//! the `LanguageSyntax` trait, allowing them to work across all supported
//! language backends.
mod constants;
mod fields;
mod tree;
pub use constants::*;
pub use fields::*;
pub use tree::*;
+154
View File
@@ -0,0 +1,154 @@
//! Shared tree generation helpers.
use std::collections::{HashMap, HashSet};
use brk_types::TreeNode;
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.
#[inline]
pub fn build_child_path(parent: &str, child: &str) -> String {
if parent.is_empty() {
child.to_string()
} else {
format!("{}/{}", parent, child)
}
}
/// Pre-computed context for a single child node.
pub struct ChildContext<'a> {
/// The child's field name in the tree.
pub name: &'a str,
/// The child node.
pub node: &'a TreeNode,
/// The field info for this child.
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.
pub is_leaf: bool,
/// Whether to use an inline type instead of a pattern type (only meaningful for branches).
pub should_inline: bool,
/// The type name to use for inline branches.
pub inline_type_name: String,
}
/// Context for generating a tree node, returned by `prepare_tree_node`.
pub struct TreeNodeContext<'a> {
/// Pre-computed context for each child.
pub children: Vec<ChildContext<'a>>,
}
/// Prepare a tree node for generation.
/// Returns None if the node should be skipped (not a branch, already generated,
/// or matches a parameterizable pattern).
///
/// The `path` parameter is the tree path to this node (e.g., "distribution/utxoCohorts").
/// It's used to look up pre-computed PatternBaseResult from the analysis phase.
pub fn prepare_tree_node<'a>(
node: &'a TreeNode,
name: &str,
path: &str,
pattern_lookup: &HashMap<Vec<PatternField>, String>,
metadata: &ClientMetadata,
generated: &mut HashSet<String>,
) -> Option<TreeNodeContext<'a>> {
let TreeNode::Branch(branch_children) = node else {
return None;
};
let fields_with_child_info = get_fields_with_child_info(branch_children, name, pattern_lookup);
let fields: Vec<PatternField> = fields_with_child_info
.iter()
.map(|(f, _)| f.clone())
.collect();
// Look up the pre-computed base result, or use a default that forces inlining
let base_result = metadata
.get_node_base(path)
.cloned()
.unwrap_or_else(PatternBaseResult::force_inline);
// Skip if this matches a parameterizable pattern AND has no outlier AND field parts match
let pattern_compatible = pattern_lookup
.get(&fields)
.and_then(|name| metadata.find_pattern(name))
.is_none_or(|p| {
p.is_suffix_mode() == base_result.is_suffix_mode
&& p.field_parts_match(&base_result.field_parts)
});
if let Some(pattern_name) = pattern_lookup.get(&fields)
&& pattern_name != name
&& metadata.is_parameterizable(pattern_name)
&& !base_result.has_outlier
&& pattern_compatible
{
return None;
}
// Skip if already generated
if generated.contains(name) {
return None;
}
generated.insert(name.to_string());
// Build child contexts with pre-computed decisions
let children: Vec<ChildContext<'a>> = branch_children
.iter()
.zip(fields_with_child_info)
.map(|((child_name, child_node), (field, child_fields))| {
let is_leaf = matches!(child_node, TreeNode::Leaf(_));
// Build child path and look up its pre-computed base result
let child_path = build_child_path(path, child_name);
let base_result = metadata
.get_node_base(&child_path)
.cloned()
.unwrap_or_else(PatternBaseResult::force_inline);
// For type annotations: use pattern type if ANY pattern matches
let matches_any_pattern = child_fields
.as_ref()
.is_some_and(|cf| metadata.matches_pattern(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)
});
// 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);
// Inline type name (only used when should_inline is true)
let inline_type_name = if should_inline {
child_type_name(name, child_name)
} else {
String::new()
};
ChildContext {
name: child_name,
node: child_node,
field,
child_fields,
base_result,
is_leaf,
should_inline,
inline_type_name,
}
})
.collect();
Some(TreeNodeContext { children })
}
@@ -0,0 +1,139 @@
//! JavaScript API method generation.
use std::fmt::Write;
use crate::{Endpoint, Parameter, generators::{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]) {
for endpoint in endpoints {
if !endpoint.should_generate() {
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)
} else {
base_return_type
};
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, " * ", " *");
}
// Add endpoint path
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();
}
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();
}
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 {
if param.required {
writeln!(
output,
" params.set('{}', String({}));",
param.name, param.name
)
.unwrap();
} else {
writeln!(
output,
" if ({} !== undefined) params.set('{}', String({}));",
param.name, param.name, 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 {
writeln!(output, " return this.getJson(path);").unwrap();
}
}
writeln!(output, " }}\n").unwrap();
}
}
fn endpoint_to_method_name(endpoint: &Endpoint) -> String {
to_camel_case(&endpoint.operation_name())
}
fn build_method_params(endpoint: &Endpoint) -> String {
let mut params = Vec::new();
for param in &endpoint.path_params {
params.push(param.name.clone());
}
for param in &endpoint.query_params {
params.push(param.name.clone());
}
params.join(", ")
}
fn build_path_template(path: &str, path_params: &[Parameter]) -> String {
let mut result = path.to_string();
for param in path_params {
let placeholder = format!("{{{}}}", param.name);
let interpolation = format!("${{{}}}", param.name);
result = result.replace(&placeholder, &interpolation);
}
result
}
/// Format param description with dash prefix, or empty string if no description.
fn format_param_desc(desc: Option<&str>) -> String {
match desc {
Some(d) if !d.is_empty() => format!(" - {}", d),
_ => String::new(),
}
}
@@ -0,0 +1,481 @@
//! JavaScript base client and pattern factory generation.
use std::fmt::Write;
use crate::{
ClientConstants, ClientMetadata, CohortConstants, GenericSyntax, IndexSetPattern,
JavaScriptSyntax, StructuralPattern, camel_case_keys, format_json,
generate_parameterized_field, to_camel_case,
};
/// Generate the base BrkClient class with HTTP functionality.
pub fn generate_base_client(output: &mut String) {
writeln!(
output,
r#"/**
* @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
*/
const _isBrowser = typeof window !== 'undefined' && 'caches' in window;
const _runIdle = (/** @type {{VoidFunction}} */ fn) => (globalThis.requestIdleCallback ?? setTimeout)(fn);
const _defaultCacheName = '__BRK_CLIENT__';
/**
* @param {{string|boolean|undefined}} cache
* @returns {{Promise<Cache | null>}}
*/
const _openCache = (cache) => {{
if (!_isBrowser || cache === false) return Promise.resolve(null);
const name = typeof cache === 'string' ? cache : _defaultCacheName;
return caches.open(name).catch(() => null);
}};
/**
* Custom error class for BRK client errors
*/
class BrkError extends Error {{
/**
* @param {{string}} message
* @param {{number}} [status]
*/
constructor(message, status) {{
super(message);
this.name = 'BrkError';
this.status = status;
}}
}}
/**
* @template T
* @typedef {{Object}} MetricData
* @property {{number}} total - Total number of data points
* @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
*/
/**
* Metric endpoint builder. Callable (returns itself) so both .by.dateindex and .by.dateindex() work.
* @template T
* @typedef {{Object}} MetricEndpointBuilder
* @property {{(index: number) => SingleItemBuilder<T>}} get - Get single item at index
* @property {{(start?: number, end?: number) => RangeBuilder<T>}} slice - Slice like Array.slice
* @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 {{() => Promise<string>}} fetchCsv - Fetch all data as CSV
* @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
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<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 {{() => 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
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<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']
* @property {{() => readonly Index[]}} indexes - Get the list of available indexes
* @property {{(index: Index) => MetricEndpointBuilder<T>|undefined}} get - Get an endpoint for a specific index
*/
/** @typedef {{MetricPattern<any>}} AnyMetricPattern */
/**
* Create a metric endpoint builder with typestate pattern.
* @template T
* @param {{BrkClientBase}} client
* @param {{string}} name - The metric vec name
* @param {{Index}} index - The index name
* @returns {{MetricEndpointBuilder<T>}}
*/
function _endpoint(client, name, index) {{
const p = `/api/metric/${{name}}/${{index}}`;
/**
* @param {{number}} [start]
* @param {{number}} [end]
* @param {{string}} [format]
* @returns {{string}}
*/
const buildPath = (start, end, format) => {{
const params = new URLSearchParams();
if (start !== undefined) params.set('start', String(start));
if (end !== undefined) params.set('end', String(end));
if (format) params.set('format', format);
const query = params.toString();
return query ? `${{p}}?${{query}}` : p;
}};
/**
* @param {{number}} [start]
* @param {{number}} [end]
* @returns {{RangeBuilder<T>}}
*/
const rangeBuilder = (start, end) => ({{
fetch(onUpdate) {{ return client.getJson(buildPath(start, end), onUpdate); }},
fetchCsv() {{ return client.getText(buildPath(start, end, 'csv')); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
}});
/**
* @param {{number}} index
* @returns {{SingleItemBuilder<T>}}
*/
const singleItemBuilder = (index) => ({{
fetch(onUpdate) {{ return client.getJson(buildPath(index, index + 1), onUpdate); }},
fetchCsv() {{ return client.getText(buildPath(index, index + 1, 'csv')); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
}});
/**
* @param {{number}} start
* @returns {{SkippedBuilder<T>}}
*/
const skippedBuilder = (start) => ({{
take(n) {{ return rangeBuilder(start, start + n); }},
fetch(onUpdate) {{ return client.getJson(buildPath(start, undefined), onUpdate); }},
fetchCsv() {{ return client.getText(buildPath(start, undefined, 'csv')); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
}});
/** @type {{MetricEndpointBuilder<T>}} */
const endpoint = {{
get(index) {{ return singleItemBuilder(index); }},
slice(start, 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); }},
fetchCsv() {{ return client.getText(buildPath(undefined, undefined, 'csv')); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
get path() {{ return p; }},
}};
return endpoint;
}}
/**
* Base HTTP client for making requests with caching support
*/
class BrkClientBase {{
/**
* @param {{BrkClientOptions|string}} options
*/
constructor(options) {{
const isString = typeof options === 'string';
this.baseUrl = isString ? options : options.baseUrl;
this.timeout = isString ? 5000 : (options.timeout ?? 5000);
/** @type {{Promise<Cache | null>}} */
this._cachePromise = _openCache(isString ? undefined : options.cache);
}}
/**
* @param {{string}} path
* @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) }});
if (!res.ok) throw new BrkError(`HTTP ${{res.status}}: ${{url}}`, res.status);
return res;
}}
/**
* Make a GET request with stale-while-revalidate caching
* @template T
* @param {{string}} path
* @param {{(value: T) => void}} [onUpdate] - Called when data is available
* @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;
if (cachedJson) onUpdate?.(cachedJson);
if (globalThis.navigator?.onLine === false) {{
if (cachedJson) return cachedJson;
throw new BrkError('Offline and no cached data available');
}}
try {{
const res = await this.get(path);
if (cachedRes?.headers.get('ETag') === res.headers.get('ETag')) return cachedJson;
const cloned = res.clone();
const json = await res.json();
onUpdate?.(json);
if (cache) _runIdle(() => cache.put(url, cloned));
return json;
}} catch (e) {{
if (cachedJson) return cachedJson;
throw e;
}}
}}
/**
* Make a GET request and return raw text (for CSV responses)
* @param {{string}} path
* @returns {{Promise<string>}}
*/
async getText(path) {{
const res = await this.get(path);
return res.text();
}}
}}
/**
* Build metric name with suffix.
* @param {{string}} acc - Accumulated prefix
* @param {{string}} s - Metric suffix
* @returns {{string}}
*/
const _m = (acc, s) => s ? (acc ? `${{acc}}_${{s}}` : s) : acc;
/**
* Build metric name with prefix.
* @param {{string}} prefix - Prefix to prepend
* @param {{string}} acc - Accumulated name
* @returns {{string}}
*/
const _p = (prefix, acc) => acc ? `${{prefix}}_${{acc}}` : prefix;
"#
)
.unwrap();
}
/// Generate static constants for the BrkClient class.
pub fn generate_static_constants(output: &mut String) {
let constants = ClientConstants::collect();
// 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));
// Cohort constants with camelCase keys
for (name, value) in CohortConstants::all() {
write_static_const(output, name, &format_json(&camel_case_keys(value)));
}
}
fn indent_json_const(json: &str) -> String {
json.lines()
.enumerate()
.map(|(i, line)| {
if i == 0 {
line.to_string()
} else {
format!(" {}", line)
}
})
.collect::<Vec<_>>()
.join("\n")
}
fn write_static_const(output: &mut String, name: &str, json: &str) {
writeln!(
output,
" {} = /** @type {{const}} */ ({});\n",
name,
indent_json_const(json)
)
.unwrap();
}
/// Generate index accessor factory functions.
pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern]) {
if patterns.is_empty() {
return;
}
writeln!(output, "// Index group constants and factory\n").unwrap();
// Generate index array constants (e.g., _i1 = ["dateindex", "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();
}
writeln!(output, "]);").unwrap();
}
writeln!(output).unwrap();
// Generate ONE generic metric pattern factory
writeln!(
output,
r#"/**
* Generic metric pattern factory.
* @template T
* @param {{BrkClientBase}} client
* @param {{string}} name - The metric vec name
* @param {{readonly Index[]}} indexes - The supported indexes
*/
function _mp(client, name, indexes) {{
const by = /** @type {{any}} */ ({{}});
for (const idx of indexes) {{
Object.defineProperty(by, idx, {{
get() {{ return _endpoint(client, name, idx); }},
enumerable: true,
configurable: true
}});
}}
return {{
name,
by,
indexes() {{ return indexes; }},
/** @param {{Index}} index */
get(index) {{ return indexes.includes(index) ? _endpoint(client, name, index) : undefined; }}
}};
}}
"#
)
.unwrap();
// Generate typedefs and thin wrapper functions
for (i, pattern) in patterns.iter().enumerate() {
// Generate typedef for type safety
let by_fields: Vec<String> = pattern
.indexes
.iter()
.map(|idx| {
format!(
"readonly {}: MetricEndpointBuilder<T>",
idx.serialize_long()
)
})
.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 }}}} {} */",
by_type, pattern.name
)
.unwrap();
// Generate thin wrapper that calls the generic factory
writeln!(
output,
"/** @template T @param {{BrkClientBase}} client @param {{string}} name @returns {{{}<T>}} */",
pattern.name
)
.unwrap();
writeln!(
output,
"function create{}(client, name) {{ return _mp(client, name, _i{}); }}",
pattern.name,
i + 1
)
.unwrap();
}
writeln!(output).unwrap();
}
/// Generate structural pattern factory functions.
pub fn generate_structural_patterns(
output: &mut String,
patterns: &[StructuralPattern],
metadata: &ClientMetadata,
) {
if patterns.is_empty() {
return;
}
writeln!(output, "// Reusable structural pattern factories\n").unwrap();
for pattern in patterns {
// Generate typedef
writeln!(output, "/**").unwrap();
if pattern.is_generic {
writeln!(output, " * @template T").unwrap();
}
writeln!(output, " * @typedef {{Object}} {}", pattern.name).unwrap();
for field in &pattern.fields {
let js_type = metadata.field_type_annotation(
field,
pattern.is_generic,
None,
GenericSyntax::JAVASCRIPT,
);
writeln!(
output,
" * @property {{{}}} {}",
js_type,
to_camel_case(&field.name)
)
.unwrap();
}
writeln!(output, " */\n").unwrap();
// Generate factory function for ALL patterns
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();
let return_type = if pattern.is_generic {
format!("{}<T>", pattern.name)
} else {
pattern.name.clone()
};
writeln!(output, " * @returns {{{}}}", return_type).unwrap();
writeln!(output, " */").unwrap();
writeln!(output, "function create{}(client, acc) {{", pattern.name).unwrap();
writeln!(output, " return {{").unwrap();
let syntax = JavaScriptSyntax;
for field in &pattern.fields {
generate_parameterized_field(output, &syntax, field, pattern, metadata, " ");
}
writeln!(output, " }};").unwrap();
writeln!(output, "}}\n").unwrap();
}
}
@@ -0,0 +1,65 @@
//! JavaScript client generation.
//!
//! This module generates a JavaScript + JSDoc client for the BRK API.
mod api;
pub mod client;
pub mod tree;
pub mod types;
use std::{fmt::Write, fs, io, path::Path};
use serde_json::json;
use crate::{ClientMetadata, Endpoint, TypeSchemas, VERSION};
/// Generate JavaScript + JSDoc client from metadata and OpenAPI endpoints.
///
/// `output_path` is the full path to the output file (e.g., "modules/brk-client/index.js").
pub fn generate_javascript_client(
metadata: &ClientMetadata,
endpoints: &[Endpoint],
schemas: &TypeSchemas,
output_path: &Path,
) -> io::Result<()> {
let mut output = String::new();
writeln!(output, "// Auto-generated BRK JavaScript client").unwrap();
writeln!(output, "// Do not edit manually\n").unwrap();
types::generate_type_definitions(&mut output, schemas);
client::generate_base_client(&mut output);
client::generate_index_accessors(&mut output, &metadata.index_set_patterns);
client::generate_structural_patterns(&mut output, &metadata.structural_patterns, metadata);
tree::generate_tree_typedefs(&mut output, &metadata.catalog, metadata);
tree::generate_main_client(&mut output, &metadata.catalog, metadata, endpoints);
fs::write(output_path, output)?;
// Update package.json version if it exists in the same directory
if let Some(parent) = output_path.parent() {
let package_json_path = parent.join("package.json");
if package_json_path.exists() {
update_package_json_version(&package_json_path)?;
}
}
Ok(())
}
fn update_package_json_version(package_json_path: &Path) -> io::Result<()> {
let content = fs::read_to_string(package_json_path)?;
let mut package: serde_json::Value = serde_json::from_str(&content)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
if let Some(obj) = package.as_object_mut() {
obj.insert("version".to_string(), json!(VERSION));
}
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")?;
Ok(())
}
@@ -0,0 +1,232 @@
//! JavaScript tree structure generation.
use std::collections::HashSet;
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,
};
use super::api::generate_api_methods;
use super::client::generate_static_constants;
/// Generate JSDoc typedefs for the metrics 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();
generate_tree_typedef(
output,
"MetricsTree",
"",
catalog,
&pattern_lookup,
metadata,
&mut generated,
);
}
fn generate_tree_typedef(
output: &mut String,
name: &str,
path: &str,
node: &TreeNode,
pattern_lookup: &std::collections::HashMap<Vec<PatternField>, String>,
metadata: &ClientMetadata,
generated: &mut HashSet<String>,
) {
let Some(ctx) = prepare_tree_node(node, name, path, pattern_lookup, metadata, generated) else {
return;
};
writeln!(output, "/**").unwrap();
writeln!(output, " * @typedef {{Object}} {}", name).unwrap();
for child in &ctx.children {
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,
)
};
writeln!(
output,
" * @property {{{}}} {}",
js_type,
to_camel_case(&child.field.name)
)
.unwrap();
}
writeln!(output, " */\n").unwrap();
// Generate child typedefs
for child in &ctx.children {
if child.should_inline {
let child_path = build_child_path(path, child.name);
generate_tree_typedef(
output,
&child.inline_type_name,
&child_path,
child.node,
pattern_lookup,
metadata,
generated,
);
}
}
}
/// Generate the main BrkClient class.
pub fn generate_main_client(
output: &mut String,
catalog: &TreeNode,
metadata: &ClientMetadata,
endpoints: &[Endpoint],
) {
let pattern_lookup = metadata.pattern_lookup();
writeln!(output, "/**").unwrap();
writeln!(
output,
" * Main BRK client with metrics tree and API methods"
)
.unwrap();
writeln!(output, " * @extends BrkClientBase").unwrap();
writeln!(output, " */").unwrap();
writeln!(output, "class BrkClient extends BrkClientBase {{").unwrap();
generate_static_constants(output);
writeln!(output, " /**").unwrap();
writeln!(output, " * @param {{BrkClientOptions|string}} options").unwrap();
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, " }}\n").unwrap();
writeln!(output, " /**").unwrap();
writeln!(output, " * @private").unwrap();
writeln!(output, " * @param {{string}} basePath").unwrap();
writeln!(output, " * @returns {{MetricsTree}}").unwrap();
writeln!(output, " */").unwrap();
writeln!(output, " _buildTree(basePath) {{").unwrap();
writeln!(output, " return {{").unwrap();
let mut generated = HashSet::new();
generate_tree_initializer(
output,
catalog,
"MetricsTree",
"",
3,
&pattern_lookup,
metadata,
&mut generated,
);
writeln!(output, " }};").unwrap();
writeln!(output, " }}\n").unwrap();
writeln!(output, " /**").unwrap();
writeln!(
output,
" * Create a dynamic metric endpoint builder for any metric/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, " *").unwrap();
writeln!(output, " * @param {{string}} metric - The metric name").unwrap();
writeln!(output, " * @param {{Index}} index - The index name").unwrap();
writeln!(output, " * @returns {{MetricEndpointBuilder<unknown>}}").unwrap();
writeln!(output, " */").unwrap();
writeln!(output, " metric(metric, index) {{").unwrap();
writeln!(output, " return _endpoint(this, metric, index);").unwrap();
writeln!(output, " }}\n").unwrap();
generate_api_methods(output, endpoints);
writeln!(output, "}}\n").unwrap();
writeln!(output, "export {{ BrkClient, BrkError }};").unwrap();
}
#[allow(clippy::too_many_arguments)]
fn generate_tree_initializer(
output: &mut String,
node: &TreeNode,
name: &str,
path: &str,
indent: usize,
pattern_lookup: &std::collections::HashMap<Vec<PatternField>, String>,
metadata: &ClientMetadata,
generated: &mut HashSet<String>,
) {
let indent_str = " ".repeat(indent);
let Some(ctx) = prepare_tree_node(node, name, path, pattern_lookup, metadata, generated) else {
return;
};
let syntax = JavaScriptSyntax;
for child in &ctx.children {
let field_name = to_camel_case(child.name);
if child.is_leaf {
if let TreeNode::Leaf(leaf) = child.node {
generate_leaf_field(
output,
&syntax,
"this",
child.name,
leaf,
metadata,
&indent_str,
);
}
} else if child.should_inline {
// Inline object
let child_path = build_child_path(path, child.name);
writeln!(output, "{}{}: {{", indent_str, field_name).unwrap();
generate_tree_initializer(
output,
child.node,
&child.inline_type_name,
&child_path,
indent + 1,
pattern_lookup,
metadata,
generated,
);
writeln!(output, "{}}},", indent_str).unwrap();
} else {
// Use pattern factory
writeln!(
output,
"{}{}: create{}(this, '{}'),",
indent_str, field_name, child.field.rust_type, child.base_result.base
)
.unwrap();
}
}
}
@@ -0,0 +1,197 @@
//! JavaScript type definitions generation.
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};
/// Generate JSDoc type definitions from OpenAPI schemas.
pub fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
if schemas.is_empty() {
return;
}
writeln!(output, "// Type definitions\n").unwrap();
for (name, schema) in schemas {
if MANUAL_GENERIC_TYPES.contains(&name.as_str()) {
continue;
}
let js_type = schema_to_js_type(schema, Some(name));
let type_desc = schema.get("description").and_then(|d| d.as_str());
if is_primitive_alias(schema) {
if let Some(desc) = type_desc {
writeln!(output, "/**").unwrap();
write_description(output, desc, " * ", " *");
writeln!(output, " *").unwrap();
writeln!(output, " * @typedef {{{}}} {}", js_type, name).unwrap();
writeln!(output, " */").unwrap();
} else {
writeln!(output, "/** @typedef {{{}}} {} */", js_type, name).unwrap();
}
} else if let Some(props) = schema.get("properties").and_then(|p| p.as_object()) {
writeln!(output, "/**").unwrap();
if let Some(desc) = type_desc {
write_description(output, desc, " * ", " *");
writeln!(output, " *").unwrap();
}
writeln!(output, " * @typedef {{Object}} {}", name).unwrap();
for (prop_name, prop_schema) in props {
let prop_type = schema_to_js_type(prop_schema, Some(name));
let required = schema
.get("required")
.and_then(|r| r.as_array())
.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 prop_desc = prop_schema
.get("description")
.and_then(|d| d.as_str())
.map(|d| format!(" - {}", d))
.unwrap_or_default();
writeln!(
output,
" * @property {{{}{}}} {}{}",
prop_type, optional, safe_name, prop_desc
)
.unwrap();
}
writeln!(output, " */").unwrap();
} else if let Some(desc) = type_desc {
writeln!(output, "/**").unwrap();
write_description(output, desc, " * ", " *");
writeln!(output, " *").unwrap();
writeln!(output, " * @typedef {{{}}} {}", js_type, name).unwrap();
writeln!(output, " */").unwrap();
} else {
writeln!(output, "/** @typedef {{{}}} {} */", js_type, name).unwrap();
}
}
writeln!(output).unwrap();
}
fn is_primitive_alias(schema: &Value) -> bool {
schema.get("properties").is_none()
&& schema.get("items").is_none()
&& schema.get("anyOf").is_none()
&& schema.get("oneOf").is_none()
&& schema.get("enum").is_none()
}
fn json_type_to_js(ty: &str, schema: &Value, current_type: Option<&str>) -> String {
match ty {
"integer" | "number" => "number".to_string(),
"boolean" => "boolean".to_string(),
"string" => "string".to_string(),
"null" => "null".to_string(),
"array" => {
let item_type = schema
.get("items")
.map(|s| schema_to_js_type(s, current_type))
.unwrap_or_else(|| "*".to_string());
format!("{}[]", item_type)
}
"object" => {
if let Some(add_props) = schema.get("additionalProperties") {
let value_type = schema_to_js_type(add_props, current_type);
return format!("{{ [key: string]: {} }}", value_type);
}
"Object".to_string()
}
_ => "*".to_string(),
}
}
/// 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()) {
for item in all_of {
let resolved = schema_to_js_type(item, current_type);
if resolved != "*" {
return resolved;
}
}
}
if let Some(ref_path) = schema.get("$ref").and_then(|r| r.as_str()) {
return ref_to_type_name(ref_path).unwrap_or("*").to_string();
}
if let Some(enum_values) = schema.get("enum").and_then(|e| e.as_array()) {
let literals: Vec<String> = enum_values
.iter()
.filter_map(|v| v.as_str())
.map(|s| format!("\"{}\"", s))
.collect();
if !literals.is_empty() {
return format!("({})", literals.join("|"));
}
}
if let Some(ty) = schema.get("type") {
if let Some(type_array) = ty.as_array() {
let types: Vec<String> = type_array
.iter()
.filter_map(|t| t.as_str())
.filter(|t| *t != "null")
.map(|t| json_type_to_js(t, schema, current_type))
.collect();
let has_null = type_array.iter().any(|t| t.as_str() == Some("null"));
if types.len() == 1 {
let base_type = &types[0];
return if has_null {
format!("?{}", base_type)
} else {
base_type.clone()
};
} else if !types.is_empty() {
let union = format!("({})", types.join("|"));
return if has_null {
format!("?{}", union)
} else {
union
};
}
}
if let Some(ty_str) = ty.as_str() {
return json_type_to_js(ty_str, schema, current_type);
}
}
if let Some(variants) = get_union_variants(schema) {
let types: Vec<String> = variants
.iter()
.map(|v| schema_to_js_type(v, current_type))
.collect();
let filtered: Vec<_> = types.iter().filter(|t| *t != "*").collect();
if !filtered.is_empty() {
return format!(
"({})",
filtered
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join("|")
);
}
return format!("({})", types.join("|"));
}
if let Some(format) = schema.get("format").and_then(|f| f.as_str()) {
return match format {
"int32" | "int64" => "number".to_string(),
"float" | "double" => "number".to_string(),
"date" | "date-time" => "string".to_string(),
_ => "*".to_string(),
};
}
"*".to_string()
}
+43
View File
@@ -0,0 +1,43 @@
//! Code generators for client libraries.
//!
//! Each language has its own submodule with focused files:
//! - `types.rs` - Type definitions
//! - `client.rs` - Base client and pattern factories
//! - `tree.rs` - Tree structure generation
//! - `api.rs` - API method generation
//! - `mod.rs` - Entry point
use std::fmt::Write;
pub mod javascript;
pub mod python;
pub mod rust;
pub use javascript::generate_javascript_client;
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"];
/// Write a multi-line description with the given prefix for each line.
/// `empty_prefix` is used for blank lines (e.g., " *" without trailing space).
pub fn write_description(output: &mut String, desc: &str, prefix: &str, empty_prefix: &str) {
for line in desc.lines() {
if line.is_empty() {
writeln!(output, "{}", empty_prefix).unwrap();
} else {
writeln!(output, "{}{}", prefix, line).unwrap();
}
}
}
/// Replace generic types with their Any variants in return types.
/// Used by JS and Python generators.
pub fn normalize_return_type(return_type: &str) -> String {
let mut result = return_type.to_string();
for type_name in MANUAL_GENERIC_TYPES {
result = result.replace(type_name, &format!("Any{}", type_name));
}
result
}
@@ -0,0 +1,199 @@
//! Python API method generation.
use std::fmt::Write;
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;
/// Generate the main client class
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.\"\"\""
)
.unwrap();
writeln!(output).unwrap();
// Generate class-level constants
generate_class_constants(output);
writeln!(
output,
" def __init__(self, base_url: str = 'http://localhost:3000', timeout: float = 30.0):"
)
.unwrap();
writeln!(output, " super().__init__(base_url, timeout)").unwrap();
writeln!(output, " self.metrics = MetricsTree(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();
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, " \"\"\"").unwrap();
writeln!(output, " return MetricEndpointBuilder(self, metric, index)").unwrap();
writeln!(output).unwrap();
// Generate API methods
generate_api_methods(output, endpoints);
}
/// Generate API methods from OpenAPI endpoints
pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
for endpoint in endpoints {
if !endpoint.should_generate() {
continue;
}
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 return_type = if endpoint.supports_csv {
format!("Union[{}, str]", base_return_type)
} else {
base_return_type
};
// Build method signature
let params = build_method_params(endpoint);
writeln!(
output,
" def {}(self{}) -> {}:",
method_name, params, return_type
)
.unwrap();
// Docstring
match (&endpoint.summary, &endpoint.description) {
(Some(summary), Some(desc)) if summary != desc => {
writeln!(output, " \"\"\"{}.", summary.trim_end_matches('.')).unwrap();
writeln!(output).unwrap();
write_description(output, desc, " ", "");
}
(Some(summary), _) => {
writeln!(output, " \"\"\"{}", summary).unwrap();
}
(None, Some(desc)) => {
// First line includes opening quotes
let mut lines = desc.lines();
if let Some(first) = lines.next() {
writeln!(output, " \"\"\"{}", first).unwrap();
}
for line in lines {
if line.is_empty() {
writeln!(output).unwrap();
} else {
writeln!(output, " {}", line).unwrap();
}
}
}
(None, None) => {
write!(output, " \"\"\"").unwrap();
}
}
writeln!(output).unwrap();
writeln!(output, " Endpoint: `{} {}`\"\"\"", endpoint.method.to_uppercase(), endpoint.path).unwrap();
// Build path
let path = build_path_template(&endpoint.path, &endpoint.path_params);
if endpoint.query_params.is_empty() {
if endpoint.path_params.is_empty() {
writeln!(output, " return self.get_json('{}')", path).unwrap();
} else {
writeln!(output, " return self.get_json(f'{}')", path).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(&param.name);
if param.required {
writeln!(
output,
" params.append(f'{}={{{}}}')",
param.name, safe_name
)
.unwrap();
} else {
writeln!(
output,
" if {} is not None: params.append(f'{}={{{}}}')",
safe_name, param.name, safe_name
)
.unwrap();
}
}
writeln!(output, " query = '&'.join(params)").unwrap();
writeln!(
output,
" path = f'{}{{\"?\" + query if query else \"\"}}'",
path
)
.unwrap();
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();
} else {
writeln!(output, " return self.get_json(path)").unwrap();
}
}
writeln!(output).unwrap();
}
}
fn endpoint_to_method_name(endpoint: &Endpoint) -> String {
to_snake_case(&endpoint.operation_name())
}
fn build_method_params(endpoint: &Endpoint) -> String {
let mut params = Vec::new();
// Path params are always required
for param in &endpoint.path_params {
let safe_name = escape_python_keyword(&param.name);
let py_type = js_type_to_python(&param.param_type);
params.push(format!(", {}: {}", safe_name, py_type));
}
// Required query params must come before optional ones (Python syntax requirement)
for param in &endpoint.query_params {
if param.required {
let safe_name = escape_python_keyword(&param.name);
let py_type = js_type_to_python(&param.param_type);
params.push(format!(", {}: {}", safe_name, py_type));
}
}
for param in &endpoint.query_params {
if !param.required {
let safe_name = escape_python_keyword(&param.name);
let py_type = js_type_to_python(&param.param_type);
params.push(format!(", {}: Optional[{}] = None", safe_name, py_type));
}
}
params.join("")
}
fn build_path_template(path: &str, path_params: &[Parameter]) -> String {
let mut result = path.to_string();
for param in path_params {
let placeholder = format!("{{{}}}", param.name);
// Use escaped name for Python variable interpolation in f-string
let safe_name = escape_python_keyword(&param.name);
let interpolation = format!("{{{}}}", safe_name);
result = result.replace(&placeholder, &interpolation);
}
result
}
@@ -0,0 +1,472 @@
//! Python base client and pattern factory generation.
use std::fmt::Write;
use crate::{
ClientConstants, ClientMetadata, CohortConstants, IndexSetPattern, PythonSyntax,
StructuralPattern, format_json, generate_parameterized_field, index_to_field_name,
};
/// Generate class-level constants for the BrkClient class.
pub fn generate_class_constants(output: &mut String) {
let constants = ClientConstants::collect();
// VERSION
writeln!(output, " VERSION = \"{}\"\n", constants.version).unwrap();
// INDEXES, POOL_ID_TO_POOL_NAME
write_class_const(output, "INDEXES", &format_json(&constants.indexes));
// Python needs string keys for pool map
let pool_map: std::collections::BTreeMap<String, &str> = constants
.pool_map
.iter()
.map(|(k, v)| (k.to_string(), *v))
.collect();
write_class_const(output, "POOL_ID_TO_POOL_NAME", &format_json(&pool_map));
// Cohort constants (no camelCase conversion for Python)
for (name, value) in CohortConstants::all() {
write_class_const(output, name, &format_json(&value));
}
}
fn write_class_const(output: &mut String, name: &str, json: &str) {
let indented = json
.lines()
.enumerate()
.map(|(i, line)| {
if i == 0 {
format!(" {} = {}", name, line)
} else {
format!(" {}", line)
}
})
.collect::<Vec<_>>()
.join("\n");
writeln!(output, "{}\n", indented).unwrap();
}
/// Generate the base BrkClient class with HTTP functionality
pub fn generate_base_client(output: &mut String) {
writeln!(
output,
r#"class BrkError(Exception):
"""Custom error class for BRK client errors."""
def __init__(self, message: str, status: Optional[int] = None):
super().__init__(message)
self.status = status
class BrkClientBase:
"""Base HTTP client for making requests."""
def __init__(self, base_url: str, timeout: float = 30.0):
parsed = urlparse(base_url)
self._host = parsed.netloc
self._secure = parsed.scheme == 'https'
self._timeout = timeout
self._conn: Optional[Union[HTTPSConnection, HTTPConnection]] = None
def _connect(self) -> Union[HTTPSConnection, HTTPConnection]:
"""Get or create HTTP connection."""
if self._conn is None:
if self._secure:
self._conn = HTTPSConnection(self._host, timeout=self._timeout)
else:
self._conn = HTTPConnection(self._host, timeout=self._timeout)
return self._conn
def get(self, path: str) -> bytes:
"""Make a GET request and return raw bytes."""
try:
conn = self._connect()
conn.request("GET", path)
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 get_json(self, path: str) -> Any:
"""Make a GET request and return JSON."""
return json.loads(self.get(path))
def get_text(self, path: str) -> str:
"""Make a GET request and return text."""
return self.get(path).decode()
def close(self):
"""Close the HTTP client."""
if self._conn:
self._conn.close()
self._conn = None
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
def _m(acc: str, s: str) -> str:
"""Build metric 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."""
return f"{{prefix}}_{{acc}}" if acc else prefix
"#
)
.unwrap();
}
/// Generate the MetricData and MetricEndpointBuilder classes
pub fn generate_endpoint_class(output: &mut String) {
writeln!(
output,
r#"class MetricData(TypedDict, Generic[T]):
"""Metric data with range information."""
total: int
start: int
end: int
data: List[T]
# Type alias for non-generic usage
AnyMetricData = MetricData[Any]
class _EndpointConfig:
"""Shared endpoint configuration."""
client: BrkClientBase
name: str
index: Index
start: Optional[int]
end: Optional[int]
def __init__(self, client: BrkClientBase, name: str, index: Index,
start: Optional[int] = None, end: Optional[int] = None):
self.client = client
self.name = name
self.index = index
self.start = start
self.end = end
def path(self) -> str:
return f"/api/metric/{{self.name}}/{{self.index}}"
def _build_path(self, format: Optional[str] = None) -> str:
params = []
if self.start is not None:
params.append(f"start={{self.start}}")
if self.end is not None:
params.append(f"end={{self.end}}")
if format is not None:
params.append(f"format={{format}}")
query = "&".join(params)
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 get_csv(self) -> str:
return self.client.get_text(self._build_path(format='csv'))
class RangeBuilder(Generic[T]):
"""Builder with range specified."""
def __init__(self, config: _EndpointConfig):
self._config = config
def fetch(self) -> MetricData[T]:
"""Fetch the range as parsed JSON."""
return self._config.get_json()
def fetch_csv(self) -> str:
"""Fetch the range as CSV string."""
return self._config.get_csv()
class SingleItemBuilder(Generic[T]):
"""Builder for single item access."""
def __init__(self, config: _EndpointConfig):
self._config = config
def fetch(self) -> MetricData[T]:
"""Fetch the single item."""
return self._config.get_json()
def fetch_csv(self) -> str:
"""Fetch as CSV."""
return self._config.get_csv()
class SkippedBuilder(Generic[T]):
"""Builder after calling skip(n). Chain with take() to specify count."""
def __init__(self, config: _EndpointConfig):
self._config = config
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
))
def fetch(self) -> MetricData[T]:
"""Fetch from skipped position to end."""
return self._config.get_json()
def fetch_csv(self) -> str:
"""Fetch as CSV."""
return self._config.get_csv()
class MetricEndpointBuilder(Generic[T]):
"""Builder for metric endpoint queries.
Use method chaining to specify the data range, then call fetch() or fetch_csv() to execute.
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.skip(100).take(10).fetch()
"""
def __init__(self, client: BrkClientBase, name: str, index: Index):
self._config = _EndpointConfig(client, name, index)
@overload
def __getitem__(self, key: int) -> SingleItemBuilder[T]: ...
@overload
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
"""
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
))
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
))
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
))
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
))
def fetch(self) -> MetricData[T]:
"""Fetch all data as parsed JSON."""
return self._config.get_json()
def fetch_csv(self) -> str:
"""Fetch all data as CSV string."""
return self._config.get_csv()
def path(self) -> str:
"""Get the base endpoint path."""
return self._config.path()
# Type alias for non-generic usage
AnyMetricEndpointBuilder = MetricEndpointBuilder[Any]
class MetricPattern(Protocol[T]):
"""Protocol for metric patterns with different index sets."""
@property
def name(self) -> str:
"""Get the metric name."""
...
def indexes(self) -> List[str]:
"""Get the list of available indexes for this metric."""
...
def get(self, index: Index) -> Optional[MetricEndpointBuilder[T]]:
"""Get an endpoint builder for a specific index, if supported."""
...
"#
)
.unwrap();
}
/// Generate index accessor classes
pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern]) {
if patterns.is_empty() {
return;
}
// Generate static index tuples
writeln!(output, "# Static index tuples").unwrap();
for (i, pattern) in patterns.iter().enumerate() {
write!(output, "_i{} = (", i + 1).unwrap();
for (j, index) in pattern.indexes.iter().enumerate() {
if j > 0 {
write!(output, ", ").unwrap();
}
write!(output, "'{}'", index.serialize_long()).unwrap();
}
// Single-element tuple needs trailing comma
if pattern.indexes.len() == 1 {
write!(output, ",").unwrap();
}
writeln!(output, ")").unwrap();
}
writeln!(output).unwrap();
// Generate helper function
writeln!(
output,
r#"def _ep(c: BrkClientBase, n: str, i: Index) -> MetricEndpointBuilder:
return MetricEndpointBuilder(c, n, i)
"#
)
.unwrap();
writeln!(output, "# Index accessor classes\n").unwrap();
for (i, pattern) in patterns.iter().enumerate() {
let by_class_name = format!("_{}By", pattern.name);
let idx_var = format!("_i{}", i + 1);
// Generate the By class with compact methods
writeln!(output, "class {}(Generic[T]):", by_class_name).unwrap();
writeln!(
output,
" def __init__(self, c: BrkClientBase, 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();
writeln!(
output,
" def {}(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, '{}')",
method_name, index_name
)
.unwrap();
}
writeln!(output).unwrap();
// Generate the main accessor class
writeln!(output, "class {}(Generic[T]):", pattern.name).unwrap();
writeln!(
output,
" def __init__(self, c: BrkClientBase, 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",
idx_var
)
.unwrap();
writeln!(output).unwrap();
}
}
/// Generate structural pattern classes
pub fn generate_structural_patterns(
output: &mut String,
patterns: &[StructuralPattern],
metadata: &ClientMetadata,
) {
if patterns.is_empty() {
return;
}
writeln!(output, "# Reusable structural pattern classes\n").unwrap();
for pattern in patterns {
// Generate class
if pattern.is_generic {
writeln!(output, "class {}(Generic[T]):", pattern.name).unwrap();
} else {
writeln!(output, "class {}:", pattern.name).unwrap();
}
writeln!(
output,
" \"\"\"Pattern struct for repeated tree structure.\"\"\""
)
.unwrap();
writeln!(output, " ").unwrap();
writeln!(
output,
" def __init__(self, client: BrkClientBase, acc: str):"
)
.unwrap();
writeln!(
output,
" \"\"\"Create pattern node with accumulated metric name.\"\"\""
)
.unwrap();
let syntax = PythonSyntax;
for field in &pattern.fields {
generate_parameterized_field(output, &syntax, field, pattern, metadata, " ");
}
writeln!(output).unwrap();
}
}
@@ -0,0 +1,48 @@
//! Python client generation.
//!
//! This module generates a Python client with type hints for the BRK API.
pub mod api;
pub mod client;
pub mod tree;
pub mod types;
use std::{fmt::Write, fs, io, path::Path};
use crate::{ClientMetadata, Endpoint, TypeSchemas};
/// Generate Python client from metadata and OpenAPI endpoints.
///
/// `output_path` is the full path to the output file (e.g., "packages/brk_client/__init__.py").
pub fn generate_python_client(
metadata: &ClientMetadata,
endpoints: &[Endpoint],
schemas: &TypeSchemas,
output_path: &Path,
) -> io::Result<()> {
let mut output = String::new();
writeln!(output, "# Auto-generated BRK Python client").unwrap();
writeln!(output, "# Do not edit manually\n").unwrap();
writeln!(
output,
"from typing import TypeVar, Generic, Any, Optional, List, Literal, TypedDict, Union, Protocol, overload"
)
.unwrap();
writeln!(output, "from http.client import HTTPSConnection, HTTPConnection").unwrap();
writeln!(output, "from urllib.parse import urlparse").unwrap();
writeln!(output, "import json\n").unwrap();
writeln!(output, "T = TypeVar('T')\n").unwrap();
types::generate_type_definitions(&mut output, schemas);
client::generate_base_client(&mut output);
client::generate_endpoint_class(&mut output);
client::generate_index_accessors(&mut output, &metadata.index_set_patterns);
client::generate_structural_patterns(&mut output, &metadata.structural_patterns, metadata);
tree::generate_tree_classes(&mut output, &metadata.catalog, metadata);
api::generate_main_client(&mut output, endpoints);
fs::write(output_path, output)?;
Ok(())
}
@@ -0,0 +1,106 @@
//! Python tree structure generation.
use std::collections::HashSet;
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,
};
/// Generate tree classes
pub fn generate_tree_classes(output: &mut String, catalog: &TreeNode, metadata: &ClientMetadata) {
writeln!(output, "# Metrics tree classes\n").unwrap();
let pattern_lookup = metadata.pattern_lookup();
let mut generated = HashSet::new();
generate_tree_class(
output,
"MetricsTree",
"",
catalog,
&pattern_lookup,
metadata,
&mut generated,
);
}
/// Recursively generate tree classes
fn generate_tree_class(
output: &mut String,
name: &str,
path: &str,
node: &TreeNode,
pattern_lookup: &std::collections::HashMap<Vec<PatternField>, String>,
metadata: &ClientMetadata,
generated: &mut HashSet<String>,
) {
let Some(ctx) = prepare_tree_node(node, name, path, pattern_lookup, metadata, generated) else {
return;
};
// Generate child classes FIRST (post-order traversal)
// This ensures children are defined before parent references them
for child in &ctx.children {
if child.should_inline {
let child_path = build_child_path(path, child.name);
generate_tree_class(
output,
&child.inline_type_name,
&child_path,
child.node,
pattern_lookup,
metadata,
generated,
);
}
}
// THEN generate the current class (after all children are defined)
writeln!(output, "class {}:", name).unwrap();
writeln!(output, " \"\"\"Metrics tree node.\"\"\"").unwrap();
writeln!(output, " ").unwrap();
writeln!(
output,
" def __init__(self, client: BrkClientBase, base_path: str = ''):"
)
.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, " ");
}
} else if child.should_inline {
// Inline class
writeln!(
output,
" self.{}: {} = {}(client)",
field_name_py, 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!(
output,
" self.{}: {} = {}(client, '{}')",
field_name_py, py_type, child.field.rust_type, child.base_result.base
)
.unwrap();
}
}
writeln!(output).unwrap();
}
@@ -0,0 +1,333 @@
//! Python type definitions generation.
use std::collections::{HashMap, HashSet};
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};
/// Generate type definitions from schemas.
pub fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
if schemas.is_empty() {
return;
}
writeln!(output, "# Type definitions\n").unwrap();
let sorted_names = topological_sort_schemas(schemas);
// Partition into simple type aliases and TypedDict classes
// Generate type aliases first to avoid forward reference issues
let (type_aliases, typed_dicts): (Vec<_>, Vec<_>) = sorted_names
.into_iter()
.filter(|name| !MANUAL_GENERIC_TYPES.contains(&name.as_str()))
.filter(|name| schemas.contains_key(name))
.partition(|name| {
schemas
.get(name)
.map(|s| s.get("properties").is_none())
.unwrap_or(false)
});
// Generate simple type aliases first
// Quote references to TypedDicts since they're defined after
let typed_dict_set: HashSet<_> = 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());
let py_type = schema_to_python_type(schema, Some(&name), Some(&typed_dict_set));
if let Some(desc) = type_desc {
for line in desc.lines() {
writeln!(output, "# {}", line).unwrap();
}
}
writeln!(output, "{} = {}", name, py_type).unwrap();
}
// Then generate TypedDict classes
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();
writeln!(output, "class {}(TypedDict):", name).unwrap();
// Collect field descriptions for Attributes section
let field_docs: Vec<(String, Option<&str>)> = props
.iter()
.map(|(prop_name, prop_schema)| {
let safe_name = escape_python_keyword(prop_name);
let desc = prop_schema.get("description").and_then(|d| d.as_str());
(safe_name, desc)
})
.collect();
let has_field_docs = field_docs.iter().any(|(_, d)| d.is_some());
// Generate docstring if we have type description or field descriptions
if type_desc.is_some() || has_field_docs {
writeln!(output, " \"\"\"").unwrap();
if let Some(desc) = type_desc {
for line in desc.lines() {
writeln!(output, " {}", line).unwrap();
}
}
if has_field_docs {
if type_desc.is_some() {
writeln!(output).unwrap();
}
writeln!(output, " Attributes:").unwrap();
for (field_name, desc) in &field_docs {
if let Some(d) = desc {
writeln!(output, " {}: {}", field_name, d).unwrap();
}
}
}
writeln!(output, " \"\"\"").unwrap();
}
for (prop_name, prop_schema) in props {
let prop_type = schema_to_python_type(prop_schema, Some(&name), None);
let safe_name = escape_python_keyword(prop_name);
writeln!(output, " {}: {}", safe_name, prop_type).unwrap();
}
writeln!(output).unwrap();
}
writeln!(output).unwrap();
}
/// Topologically sort schema names so dependencies come before dependents (avoids forward references).
/// 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();
for (name, schema) in schemas {
let mut type_deps = HashSet::new();
collect_schema_refs(schema, &mut type_deps);
// Only keep deps that are in our schemas
type_deps.retain(|d| schemas.contains_key(d));
deps.insert(name.clone(), type_deps);
}
// Kahn's algorithm for topological sort
let mut in_degree: HashMap<String, usize> = HashMap::new();
for name in schemas.keys() {
in_degree.insert(name.clone(), 0);
}
for type_deps in deps.values() {
for dep in type_deps {
*in_degree.entry(dep.clone()).or_insert(0) += 1;
}
}
// Start with types that have no dependents (are not referenced by others)
let mut queue: Vec<String> = in_degree
.iter()
.filter(|(_, count)| **count == 0)
.map(|(name, _)| name.clone())
.collect();
queue.sort(); // Deterministic order
let mut result = Vec::new();
while let Some(name) = queue.pop() {
result.push(name.clone());
if let Some(type_deps) = deps.get(&name) {
for dep in type_deps {
if let Some(count) = in_degree.get_mut(dep) {
*count = count.saturating_sub(1);
if *count == 0 {
queue.push(dep.clone());
queue.sort(); // Keep sorted for determinism
}
}
}
}
}
// Reverse so dependencies come first
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 mut missing: Vec<_> = schemas
.keys()
.filter(|k| !result_set.contains(*k))
.cloned()
.collect();
missing.sort();
result.extend(missing);
result
}
/// Collect all type references ($ref) from a schema
fn collect_schema_refs(schema: &Value, refs: &mut HashSet<String>) {
match schema {
Value::Object(map) => {
if let Some(ref_path) = map.get("$ref").and_then(|r| r.as_str())
&& let Some(type_name) = ref_to_type_name(ref_path)
{
refs.insert(type_name.to_string());
}
for value in map.values() {
collect_schema_refs(value, refs);
}
}
Value::Array(arr) => {
for item in arr {
collect_schema_refs(item, refs);
}
}
_ => {}
}
}
/// Convert a single JSON type string to Python type
fn json_type_to_python(ty: &str, schema: &Value, current_type: Option<&str>) -> String {
match ty {
"integer" => "int".to_string(),
"number" => "float".to_string(),
"boolean" => "bool".to_string(),
"string" => "str".to_string(),
"null" => "None".to_string(),
"array" => {
let item_type = schema
.get("items")
.map(|s| schema_to_python_type(s, current_type, None))
.unwrap_or_else(|| "Any".to_string());
format!("List[{}]", item_type)
}
"object" => {
if let Some(add_props) = schema.get("additionalProperties") {
let value_type = schema_to_python_type(add_props, current_type, None);
return format!("dict[str, {}]", value_type);
}
"dict".to_string()
}
_ => "Any".to_string(),
}
}
/// Convert JSON Schema to Python type.
///
/// - `current_type`: Used to detect and quote self-references for recursive types
/// - `quote_types`: Optional set of additional type names that should be quoted
pub fn schema_to_python_type(
schema: &Value,
current_type: Option<&str>,
quote_types: Option<&HashSet<String>>,
) -> String {
if let Some(all_of) = schema.get("allOf").and_then(|v| v.as_array()) {
for item in all_of {
let resolved = schema_to_python_type(item, current_type, quote_types);
if resolved != "Any" {
return resolved;
}
}
}
// Handle $ref
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));
if should_quote {
return format!("\"{}\"", type_name);
}
return type_name.to_string();
}
// Handle enum (array of string values)
if let Some(enum_values) = schema.get("enum").and_then(|e| e.as_array()) {
let literals: Vec<String> = enum_values
.iter()
.filter_map(|v| v.as_str())
.map(|s| format!("\"{}\"", s))
.collect();
if !literals.is_empty() {
return format!("Literal[{}]", literals.join(", "));
}
}
if let Some(ty) = schema.get("type") {
if let Some(type_array) = ty.as_array() {
let types: Vec<String> = type_array
.iter()
.filter_map(|t| t.as_str())
.filter(|t| *t != "null")
.map(|t| json_type_to_python(t, schema, current_type))
.collect();
let has_null = type_array.iter().any(|t| t.as_str() == Some("null"));
if types.len() == 1 {
let base_type = &types[0];
return if has_null {
format!("Optional[{}]", base_type)
} else {
base_type.clone()
};
} else if !types.is_empty() {
let union = format!("Union[{}]", types.join(", "));
return if has_null {
format!("Optional[{}]", union)
} else {
union
};
}
}
if let Some(ty_str) = ty.as_str() {
return json_type_to_python(ty_str, schema, current_type);
}
}
if let Some(variants) = get_union_variants(schema) {
let types: Vec<String> = variants
.iter()
.map(|v| schema_to_python_type(v, current_type, quote_types))
.collect();
let filtered: Vec<_> = types.iter().filter(|t| *t != "Any").collect();
if !filtered.is_empty() {
return format!(
"Union[{}]",
filtered
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(", ")
);
}
return format!("Union[{}]", types.join(", "));
}
// Check for format hint without type (common in OpenAPI)
if let Some(format) = schema.get("format").and_then(|f| f.as_str()) {
return match format {
"int32" | "int64" => "int".to_string(),
"float" | "double" => "float".to_string(),
"date" | "date-time" => "str".to_string(),
_ => "Any".to_string(),
};
}
"Any".to_string()
}
/// Convert JS-style type to Python type (e.g., "Txid[]" -> "List[Txid]", "integer" -> "int")
pub fn js_type_to_python(js_type: &str) -> String {
if let Some(inner) = js_type.strip_suffix("[]") {
format!("List[{}]", js_type_to_python(inner))
} else {
match js_type {
"integer" => "int".to_string(),
"number" => "float".to_string(),
"boolean" => "bool".to_string(),
"string" => "str".to_string(),
"null" => "None".to_string(),
"Object" | "object" => "dict".to_string(),
"*" => "Any".to_string(),
_ => js_type.to_string(),
}
}
}
@@ -0,0 +1,194 @@
//! Rust API method generation.
use std::fmt::Write;
use crate::{Endpoint, VERSION, generators::write_description, to_snake_case};
use super::types::js_type_to_rust;
/// Generate the main BrkClient struct.
pub fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) {
writeln!(
output,
r#"/// Main BRK client with metrics tree and API methods.
pub struct BrkClient {{
base: Arc<BrkClientBase>,
metrics: MetricsTree,
}}
impl BrkClient {{
/// Client version.
pub const VERSION: &'static str = "v{VERSION}";
/// 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 }}
}}
/// 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 }}
}}
/// Get the metrics tree for navigating metrics.
pub fn metrics(&self) -> &MetricsTree {{
&self.metrics
}}
/// Create a dynamic metric endpoint builder for any metric/index combination.
///
/// Use this for programmatic access when the metric name is determined at runtime.
/// For type-safe access, use the `metrics()` tree instead.
///
/// # Example
/// ```ignore
/// let data = client.metric("realized_price", Index::Height)
/// .last(10)
/// .json::<f64>()?;
/// ```
pub fn metric(&self, metric: impl Into<Metric>, index: Index) -> MetricEndpointBuilder<serde_json::Value> {{
MetricEndpointBuilder::new(
self.base.clone(),
Arc::from(metric.into().as_str()),
index,
)
}}
"#,
VERSION = VERSION
)
.unwrap();
generate_api_methods(output, endpoints);
writeln!(output, "}}").unwrap();
}
/// Generate API methods from OpenAPI endpoints.
pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
for endpoint in endpoints {
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, " /// ", " ///");
}
// 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 endpoint_to_method_name(endpoint: &Endpoint) -> String {
to_snake_case(&endpoint.operation_name())
}
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(&param.param_type);
params.push(format!(", {}: {}", param.name, rust_type));
}
for param in &endpoint.query_params {
let rust_type = param_type_to_rust(&param.param_type);
if param.required {
params.push(format!(", {}: {}", param.name, rust_type));
} else {
params.push(format!(", {}: Option<{}>", param.name, rust_type));
}
}
params.join("")
}
/// Convert parameter type to Rust type for function signatures.
fn param_type_to_rust(param_type: &str) -> String {
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
}
}
/// 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");
if has_index_param {
(endpoint.path.replace("{index}", "{}"), ", index.serialize_long()")
} else {
(endpoint.path.clone(), "")
}
}
@@ -0,0 +1,525 @@
//! Rust base client and pattern factory generation.
use std::fmt::Write;
use crate::{
ClientMetadata, GenericSyntax, IndexSetPattern, RustSyntax, StructuralPattern,
generate_parameterized_field, index_to_field_name, to_snake_case,
};
/// Generate import statements.
pub fn generate_imports(output: &mut String) {
writeln!(
output,
r#"use std::sync::Arc;
use std::ops::{{Bound, RangeBounds}};
use serde::de::DeserializeOwned;
pub use brk_cohort::*;
pub use brk_types::*;
"#
)
.unwrap();
}
/// Generate the base BrkClientBase struct and error types.
pub fn generate_base_client(output: &mut String) {
writeln!(
output,
r#"/// Error type for BRK client operations.
#[derive(Debug)]
pub struct BrkError {{
pub message: String,
}}
impl std::fmt::Display for BrkError {{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{
write!(f, "{{}}", self.message)
}}
}}
impl std::error::Error for BrkError {{}}
/// Result type for BRK client operations.
pub type Result<T> = std::result::Result<T, BrkError>;
/// Options for configuring the BRK client.
#[derive(Debug, Clone)]
pub struct BrkClientOptions {{
pub base_url: String,
pub timeout_secs: u64,
}}
impl Default for BrkClientOptions {{
fn default() -> Self {{
Self {{
base_url: "http://localhost:3000".to_string(),
timeout_secs: 30,
}}
}}
}}
/// Base HTTP client for making requests.
#[derive(Debug, Clone)]
pub struct BrkClientBase {{
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,
}}
}}
/// Create a new client with options.
pub fn with_options(options: BrkClientOptions) -> Self {{
Self {{
base_url: options.base_url,
timeout_secs: options.timeout_secs,
}}
}}
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)
}}
/// Make a GET request and deserialize JSON response.
pub fn get_json<T: DeserializeOwned>(&self, path: &str) -> Result<T> {{
self.get(path)?
.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())
.map_err(|e| BrkError {{ message: e.to_string() }})
}}
}}
/// Build metric name with suffix.
#[inline]
fn _m(acc: &str, s: &str) -> String {{
if s.is_empty() {{ acc.to_string() }}
else if acc.is_empty() {{ s.to_string() }}
else {{ format!("{{acc}}_{{s}}") }}
}}
/// Build metric name with prefix.
#[inline]
fn _p(prefix: &str, acc: &str) -> String {{
if acc.is_empty() {{ prefix.to_string() }} else {{ format!("{{prefix}}_{{acc}}") }}
}}
"#
)
.unwrap();
}
/// Generate the MetricPattern trait.
pub fn generate_metric_pattern_trait(output: &mut String) {
writeln!(
output,
r#"/// Non-generic trait for metric patterns (usable in collections).
pub trait AnyMetricPattern {{
/// Get the metric name.
fn name(&self) -> &str;
/// Get the list of available indexes for this metric.
fn indexes(&self) -> &'static [Index];
}}
/// Generic trait for metric patterns with endpoint access.
pub trait MetricPattern<T>: AnyMetricPattern {{
/// Get an endpoint builder for a specific index, if supported.
fn get(&self, index: Index) -> Option<MetricEndpointBuilder<T>>;
}}
"#
)
.unwrap();
}
/// Generate the MetricEndpointBuilder structs with typestate pattern.
pub fn generate_endpoint(output: &mut String) {
writeln!(
output,
r#"/// Shared endpoint configuration.
#[derive(Clone)]
struct EndpointConfig {{
client: Arc<BrkClientBase>,
name: Arc<str>,
index: Index,
start: Option<i64>,
end: Option<i64>,
}}
impl EndpointConfig {{
fn new(client: Arc<BrkClientBase>, name: Arc<str>, index: Index) -> Self {{
Self {{ client, name, index, start: None, end: None }}
}}
fn path(&self) -> String {{
format!("/api/metric/{{}}/{{}}", self.name, self.index.serialize_long())
}}
fn build_path(&self, format: Option<&str>) -> String {{
let mut params = Vec::new();
if let Some(s) = self.start {{ params.push(format!("start={{}}", s)); }}
if let Some(e) = self.end {{ params.push(format!("end={{}}", e)); }}
if let Some(fmt) = format {{ params.push(format!("format={{}}", fmt)); }}
let p = self.path();
if params.is_empty() {{ p }} else {{ format!("{{}}?{{}}", p, params.join("&")) }}
}}
fn get_json<T: DeserializeOwned>(&self, format: Option<&str>) -> Result<T> {{
self.client.get_json(&self.build_path(format))
}}
fn get_text(&self, format: Option<&str>) -> Result<String> {{
self.client.get_text(&self.build_path(format))
}}
}}
/// Initial builder for metric endpoint queries.
///
/// Use method chaining to specify the data range, then call `fetch()` or `fetch_csv()` to execute.
///
/// # 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()?;
/// ```
pub struct MetricEndpointBuilder<T> {{
config: EndpointConfig,
_marker: std::marker::PhantomData<T>,
}}
impl<T: DeserializeOwned> MetricEndpointBuilder<T> {{
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> {{
self.config.start = Some(index as i64);
self.config.end = Some(index as i64 + 1);
SingleItemBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
}}
/// Select a range using Rust range syntax.
///
/// # Examples
/// ```ignore
/// endpoint.range(..10) // first 10
/// 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> {{
self.config.start = match range.start_bound() {{
Bound::Included(&n) => Some(n as i64),
Bound::Excluded(&n) => Some(n as i64 + 1),
Bound::Unbounded => None,
}};
self.config.end = match range.end_bound() {{
Bound::Included(&n) => Some(n as i64 + 1),
Bound::Excluded(&n) => Some(n as i64),
Bound::Unbounded => None,
}};
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
}}
/// Take the first n items.
pub fn take(self, n: usize) -> RangeBuilder<T> {{
self.range(..n)
}}
/// Take the last n items.
pub fn last(mut self, n: usize) -> RangeBuilder<T> {{
if n == 0 {{
self.config.end = Some(0);
}} else {{
self.config.start = Some(-(n as i64));
}}
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
}}
/// Skip the first n items. Chain with `take(n)` to get a range.
pub fn skip(mut self, n: usize) -> SkippedBuilder<T> {{
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>> {{
self.config.get_json(None)
}}
/// Fetch all data as CSV string.
pub fn fetch_csv(self) -> Result<String> {{
self.config.get_text(Some("csv"))
}}
/// 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>,
}}
impl<T: DeserializeOwned> SingleItemBuilder<T> {{
/// Fetch the single item.
pub fn fetch(self) -> Result<MetricData<T>> {{
self.config.get_json(None)
}}
/// Fetch the single item as CSV.
pub fn fetch_csv(self) -> Result<String> {{
self.config.get_text(Some("csv"))
}}
}}
/// Builder after calling `skip(n)`. Chain with `take(n)` to specify count.
pub struct SkippedBuilder<T> {{
config: EndpointConfig,
_marker: std::marker::PhantomData<T>,
}}
impl<T: DeserializeOwned> SkippedBuilder<T> {{
/// Take n items after the skipped position.
pub fn take(mut self, n: usize) -> RangeBuilder<T> {{
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>> {{
self.config.get_json(None)
}}
/// Fetch from the skipped position to the end as CSV.
pub fn fetch_csv(self) -> Result<String> {{
self.config.get_text(Some("csv"))
}}
}}
/// Builder with range fully specified.
pub struct RangeBuilder<T> {{
config: EndpointConfig,
_marker: std::marker::PhantomData<T>,
}}
impl<T: DeserializeOwned> RangeBuilder<T> {{
/// Fetch the range as parsed JSON.
pub fn fetch(self) -> Result<MetricData<T>> {{
self.config.get_json(None)
}}
/// Fetch the range as CSV string.
pub fn fetch_csv(self) -> Result<String> {{
self.config.get_text(Some("csv"))
}}
}}
"#
)
.unwrap();
}
/// Generate index accessor structs.
pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern]) {
if patterns.is_empty() {
return;
}
// Generate static index arrays
writeln!(output, "// Static index arrays").unwrap();
for (i, pattern) in patterns.iter().enumerate() {
write!(output, "const _I{}: &[Index] = &[", i + 1).unwrap();
for (j, index) in pattern.indexes.iter().enumerate() {
if j > 0 {
write!(output, ", ").unwrap();
}
write!(output, "Index::{}", index).unwrap();
}
writeln!(output, "];").unwrap();
}
writeln!(output).unwrap();
// Generate helper function
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)
}}
"#
)
.unwrap();
// Generate index accessor structs
writeln!(output, "// Index accessor structs\n").unwrap();
for (i, pattern) in patterns.iter().enumerate() {
let by_name = format!("{}By", pattern.name);
let idx_const = format!("_I{}", i + 1);
// Generate the "By" struct
writeln!(output, "pub struct {}<T> {{ client: Arc<BrkClientBase>, name: Arc<str>, _marker: std::marker::PhantomData<T> }}", by_name).unwrap();
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();
}
writeln!(output, "}}\n").unwrap();
// Generate the main accessor struct
writeln!(
output,
"pub struct {}<T> {{ name: Arc<str>, pub by: {}<T> }}",
pattern.name, by_name
)
.unwrap();
writeln!(output, "impl<T: DeserializeOwned> {}<T> {{", pattern.name).unwrap();
writeln!(
output,
" pub fn new(client: Arc<BrkClientBase>, name: String) -> Self {{ let name: Arc<str> = name.into(); Self {{ name: name.clone(), by: {} {{ client, name, _marker: std::marker::PhantomData }} }} }}",
by_name
)
.unwrap();
writeln!(output, " pub fn name(&self) -> &str {{ &self.name }}").unwrap();
writeln!(output, "}}\n").unwrap();
// Implement AnyMetricPattern trait
writeln!(
output,
"impl<T> AnyMetricPattern for {}<T> {{ fn name(&self) -> &str {{ &self.name }} fn indexes(&self) -> &'static [Index] {{ {} }} }}",
pattern.name, idx_const
)
.unwrap();
// Implement MetricPattern<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",
pattern.name, idx_const
)
.unwrap();
}
}
/// Generate structural pattern structs.
pub fn generate_pattern_structs(
output: &mut String,
patterns: &[StructuralPattern],
metadata: &ClientMetadata,
) {
if patterns.is_empty() {
return;
}
writeln!(output, "// Reusable pattern structs\n").unwrap();
for pattern in patterns {
let generic_params = if pattern.is_generic { "<T>" } else { "" };
// Generate struct definition
writeln!(output, "/// Pattern struct for repeated tree structure.").unwrap();
writeln!(output, "pub struct {}{} {{", pattern.name, generic_params).unwrap();
for field in &pattern.fields {
let field_name = to_snake_case(&field.name);
let type_annotation = metadata.field_type_annotation(
field,
pattern.is_generic,
None,
GenericSyntax::RUST,
);
writeln!(output, " pub {}: {},", field_name, type_annotation).unwrap();
}
writeln!(output, "}}\n").unwrap();
// Generate impl block with constructor for ALL patterns
let impl_generic = if pattern.is_generic {
"<T: DeserializeOwned>"
} else {
""
};
writeln!(
output,
"impl{} {}{} {{",
impl_generic, pattern.name, generic_params
)
.unwrap();
writeln!(
output,
" /// Create a new pattern node with accumulated metric name."
)
.unwrap();
writeln!(
output,
" pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {{"
)
.unwrap();
writeln!(output, " Self {{").unwrap();
let syntax = RustSyntax;
for field in &pattern.fields {
generate_parameterized_field(output, &syntax, field, pattern, metadata, " ");
}
writeln!(output, " }}").unwrap();
writeln!(output, " }}").unwrap();
writeln!(output, "}}\n").unwrap();
}
}
@@ -0,0 +1,44 @@
//! Rust client generation.
//!
//! This module generates a Rust client with full type safety for the BRK API.
pub mod api;
pub mod client;
pub mod tree;
mod types;
use std::{fmt::Write, fs, io, path::Path};
use crate::{ClientMetadata, Endpoint};
/// Generate Rust client from metadata and OpenAPI endpoints.
///
/// `output_path` is the full path to the output file (e.g., "crates/brk_client/src/lib.rs").
pub fn generate_rust_client(
metadata: &ClientMetadata,
endpoints: &[Endpoint],
output_path: &Path,
) -> io::Result<()> {
let mut output = String::new();
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(dead_code)]").unwrap();
writeln!(output, "#![allow(unused_variables)]").unwrap();
writeln!(output, "#![allow(clippy::useless_format)]").unwrap();
writeln!(output, "#![allow(clippy::unnecessary_to_owned)]\n").unwrap();
client::generate_imports(&mut output);
client::generate_base_client(&mut output);
client::generate_metric_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)?;
Ok(())
}
@@ -0,0 +1,134 @@
//! Rust tree structure generation.
use std::collections::HashSet;
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,
};
/// Generate tree structs.
pub fn generate_tree(output: &mut String, catalog: &TreeNode, metadata: &ClientMetadata) {
writeln!(output, "// Metrics tree\n").unwrap();
let pattern_lookup = metadata.pattern_lookup();
let mut generated = HashSet::new();
generate_tree_node(
output,
"MetricsTree",
"",
catalog,
&pattern_lookup,
metadata,
&mut generated,
);
}
fn generate_tree_node(
output: &mut String,
name: &str,
path: &str,
node: &TreeNode,
pattern_lookup: &std::collections::HashMap<Vec<PatternField>, String>,
metadata: &ClientMetadata,
generated: &mut HashSet<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, "pub struct {} {{", name).unwrap();
for child in &ctx.children {
let field_name = 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,
)
};
writeln!(output, " pub {}: {},", field_name, type_annotation).unwrap();
}
writeln!(output, "}}\n").unwrap();
// Generate impl block
writeln!(output, "impl {} {{", name).unwrap();
writeln!(
output,
" pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {{"
)
.unwrap();
writeln!(output, " Self {{").unwrap();
let syntax = RustSyntax;
for child in &ctx.children {
let field_name = to_snake_case(child.name);
if child.is_leaf {
if let TreeNode::Leaf(leaf) = child.node {
generate_leaf_field(
output,
&syntax,
"client.clone()",
child.name,
leaf,
metadata,
" ",
);
}
} else if child.should_inline {
// Inline struct type - only for nodes that don't match any pattern
let path_expr = syntax.path_expr("base_path", &format!("_{}", child.name));
writeln!(
output,
" {}: {}::new(client.clone(), {}),",
field_name, child.inline_type_name, path_expr
)
.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),
);
}
}
writeln!(output, " }}").unwrap();
writeln!(output, " }}").unwrap();
writeln!(output, "}}\n").unwrap();
// Generate child structs
for child in &ctx.children {
if child.should_inline {
let child_path = build_child_path(path, child.name);
generate_tree_node(
output,
&child.inline_type_name,
&child_path,
child.node,
pattern_lookup,
metadata,
generated,
);
}
}
}
@@ -0,0 +1,17 @@
//! Rust type conversion utilities.
/// Convert JS-style type to Rust type.
pub fn js_type_to_rust(js_type: &str) -> String {
if let Some(inner) = js_type.strip_suffix("[]") {
format!("Vec<{}>", js_type_to_rust(inner))
} else {
match js_type {
"string" => "String".to_string(),
"integer" => "i64".to_string(),
"number" => "f64".to_string(),
"boolean" => "bool".to_string(),
"*" => "serde_json::Value".to_string(),
other => other.to_string(),
}
}
}
+187
View File
@@ -0,0 +1,187 @@
#![allow(clippy::type_complexity)]
use std::{collections::btree_map::Entry, fs::create_dir_all, io, path::PathBuf};
use brk_query::Vecs;
/// Output path configuration for each language client.
///
/// Each path should be the full path to the output file, not just a directory.
/// Parent directories will be created automatically if they don't exist.
///
/// # Example
/// ```ignore
/// let paths = ClientOutputPaths::new()
/// .rust("crates/brk_client/src/lib.rs")
/// .javascript("modules/brk-client/index.js")
/// .python("packages/brk_client/__init__.py");
/// ```
#[derive(Debug, Clone, Default)]
pub struct ClientOutputPaths {
/// Full path to Rust client file (e.g., "crates/brk_client/src/lib.rs")
pub rust: Option<PathBuf>,
/// Full path to JavaScript client file (e.g., "modules/brk-client/index.js")
pub javascript: Option<PathBuf>,
/// Full path to Python client file (e.g., "packages/brk_client/__init__.py")
pub python: Option<PathBuf>,
}
impl ClientOutputPaths {
pub fn new() -> Self {
Self::default()
}
pub fn rust(mut self, path: impl Into<PathBuf>) -> Self {
self.rust = Some(path.into());
self
}
pub fn javascript(mut self, path: impl Into<PathBuf>) -> Self {
self.javascript = Some(path.into());
self
}
pub fn python(mut self, path: impl Into<PathBuf>) -> Self {
self.python = Some(path.into());
self
}
}
mod analysis;
mod backends;
mod generate;
mod generators;
mod openapi;
mod syntax;
mod types;
pub use analysis::*;
pub use backends::*;
pub use generate::*;
pub use generators::*;
pub use openapi::*;
pub use syntax::*;
pub use types::*;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
/// Generate all client libraries from the query vecs and OpenAPI JSON.
///
/// Uses `ClientOutputPaths` to specify the output file path for each language.
/// Only languages with a configured path will be generated.
///
/// # Example
/// ```ignore
/// let paths = ClientOutputPaths::new()
/// .rust("crates/brk_client/src/lib.rs")
/// .javascript("modules/brk-client/index.js")
/// .python("packages/brk_client/__init__.py");
///
/// generate_clients(&vecs, &openapi_json, &paths)?;
/// ```
pub fn generate_clients(
vecs: &Vecs,
openapi_json: &str,
output_paths: &ClientOutputPaths,
) -> io::Result<()> {
let metadata = ClientMetadata::from_vecs(vecs);
// Parse OpenAPI spec
let spec = parse_openapi_json(openapi_json)?;
let endpoints = extract_endpoints(&spec);
let mut schemas = extract_schemas(openapi_json);
// Collect leaf type schemas from the catalog and merge into schemas
collect_leaf_type_schemas(&metadata.catalog, &mut schemas);
// Also collect definitions from all schemas (including OpenAPI schemas)
// We need to do this after collecting leaf schemas so we process everything
let schema_values: Vec<_> = schemas.values().cloned().collect();
for schema in &schema_values {
collect_schema_definitions(schema, &mut schemas);
}
// Generate Rust client (uses real brk_types, no schema conversion needed)
if let Some(rust_path) = &output_paths.rust {
if let Some(parent) = rust_path.parent() {
create_dir_all(parent)?;
}
generate_rust_client(&metadata, &endpoints, rust_path)?;
}
// Generate JavaScript client (needs schemas for type definitions)
if let Some(js_path) = &output_paths.javascript {
if let Some(parent) = js_path.parent() {
create_dir_all(parent)?;
}
generate_javascript_client(&metadata, &endpoints, &schemas, js_path)?;
}
// Generate Python client (needs schemas for type definitions)
if let Some(python_path) = &output_paths.python {
if let Some(parent) = python_path.parent() {
create_dir_all(parent)?;
}
generate_python_client(&metadata, &endpoints, &schemas, python_path)?;
}
Ok(())
}
use brk_types::TreeNode;
use serde_json::Value;
/// Recursively collect leaf type schemas from the tree and add to schemas map.
/// Only adds schemas that aren't already present (OpenAPI schemas take precedence).
/// Collects definitions from schemars-generated schemas (for referenced types).
fn collect_leaf_type_schemas(node: &TreeNode, schemas: &mut TypeSchemas) {
match node {
TreeNode::Leaf(leaf) => {
// Collect definitions from the schema (schemars puts type schemas here)
// This includes the inner types like `Bitcoin` from `Close<Bitcoin>`
collect_schema_definitions(&leaf.schema, schemas);
// Get the type name for this leaf
let type_name = extract_inner_type(leaf.kind());
if let Entry::Vacant(e) = schemas.entry(type_name) {
// Unwrap single-element allOf
let schema = unwrap_allof(&leaf.schema);
// Add the schema if it's usable:
// - Simple type (has "type")
// - Object type with properties (complex types like OHLCCents, EmptyAddressData)
// - Enum type (has "enum" or "oneOf")
// - Or a $ref to another type
let has_type = schema.get("type").is_some();
let has_properties = schema.get("properties").is_some();
let has_enum = schema.get("enum").is_some() || schema.get("oneOf").is_some();
let is_ref = schema.get("$ref").is_some();
if has_type || has_properties || has_enum || is_ref {
e.insert(schema.clone());
}
}
}
TreeNode::Branch(children) => {
for child in children.values() {
collect_leaf_type_schemas(child, schemas);
}
}
}
}
/// Collect type definitions from schemars-generated schema's definitions section.
/// Schemars uses `definitions` or `$defs` to store referenced types.
fn collect_schema_definitions(schema: &Value, schemas: &mut TypeSchemas) {
// Check both JSON Schema draft-07 style ("definitions") and draft 2019-09+ style ("$defs")
for key in ["definitions", "$defs"] {
if let Some(defs) = schema.get(key).and_then(|d| d.as_object()) {
for (name, def_schema) in defs {
if !schemas.contains_key(name) {
schemas.insert(name.clone(), def_schema.clone());
}
}
}
}
}
+340
View File
@@ -0,0 +1,340 @@
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()),
}
}
+95
View File
@@ -0,0 +1,95 @@
//! Language-specific syntax traits for code generation.
//!
//! This module defines the `LanguageSyntax` trait that abstracts over
//! language-specific code generation patterns, allowing shared generation
//! logic to work across Python, JavaScript, and Rust backends.
use crate::GenericSyntax;
/// Language-specific syntax for code generation.
///
/// Implementations of this trait provide the language-specific formatting
/// for generated client code. This allows the core generation logic to be
/// written once and reused across all supported languages.
pub trait LanguageSyntax {
/// Convert a field name to the language's naming convention.
///
/// - Python/Rust: `snake_case`
/// - JavaScript: `camelCase`
fn field_name(&self, name: &str) -> String;
/// Format an interpolated path expression.
///
/// # Arguments
/// * `base_var` - The variable name to interpolate (e.g., "acc", "base_path")
/// * `suffix` - The suffix to append (e.g., "_field_name")
///
/// # Returns
/// - Python: `f'{acc}_suffix'`
/// - JavaScript: `` `${acc}_suffix` ``
/// - Rust: `format!("{acc}_suffix")`
fn path_expr(&self, base_var: &str, suffix: &str) -> String;
/// Format a suffix mode expression: `_m(acc, relative)`.
///
/// Suffix mode appends the relative name to the accumulator.
/// - If relative is empty, returns just acc (identity)
/// - Otherwise: `{acc}_{relative}` or `{relative}` if acc is empty
///
/// # Arguments
/// * `acc_var` - The accumulator variable name (e.g., "acc")
/// * `relative` - The relative name to append (e.g., "max_cost_basis")
fn suffix_expr(&self, acc_var: &str, relative: &str) -> String;
/// Format a prefix mode expression: `_p(prefix, acc)`.
///
/// Prefix mode prepends the prefix to the accumulator.
/// - If prefix is empty, returns just acc (identity)
/// - Otherwise: `{prefix}{acc}` (prefix includes trailing underscore)
///
/// # Arguments
/// * `prefix` - The prefix to prepend (e.g., "cumulative_")
/// * `acc_var` - The accumulator variable name (e.g., "acc")
fn prefix_expr(&self, prefix: &str, acc_var: &str) -> String;
/// Generate a constructor call for patterns and accessors.
///
/// - Python: `TypeName(client, path)`
/// - JavaScript: `createTypeName(client, path)`
/// - Rust: `TypeName::new(client.clone(), path)`
fn constructor(&self, type_name: &str, path_expr: &str) -> String;
/// Generate a field initialization line.
///
/// # Arguments
/// * `indent` - The indentation string
/// * `name` - The field name (already converted to language convention)
/// * `type_ann` - The type annotation (may be ignored by some languages)
/// * `value` - The initialization value/expression
///
/// # Returns
/// - Python: `{indent}self.{name}: {type_ann} = {value}`
/// - JavaScript: `{indent}{name}: {value},`
/// - Rust: `{indent}{name}: {value},`
fn field_init(&self, indent: &str, name: &str, type_ann: &str, value: &str) -> String;
/// Get the generic type syntax for this language.
///
/// - Python: `[T]` with default `Any`
/// - JavaScript: `<T>` with default `unknown`
/// - Rust: `<T>` with default `_`
fn generic_syntax(&self) -> GenericSyntax;
/// Format a string literal.
///
/// - Python/JavaScript: `'value'` (single quotes)
/// - Rust: `"value"` (double quotes)
fn string_literal(&self, value: &str) -> String;
/// Get the constructor name/prefix for a type.
///
/// - Python: `TypeName`
/// - JavaScript: `createTypeName`
/// - Rust: `TypeName::new`
fn constructor_name(&self, type_name: &str) -> String;
}
+90
View File
@@ -0,0 +1,90 @@
use brk_types::Index;
/// Convert a string to PascalCase (e.g., "fee_rate" -> "FeeRate").
pub fn to_pascal_case(s: &str) -> String {
s.replace('-', "_")
.split('_')
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect()
}
/// Convert a string to snake_case, handling Rust keywords.
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()) {
format!("_{}", sanitized)
} else {
sanitized
};
// Handle Rust keywords
match sanitized.as_str() {
"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,
}
}
/// Convert a string to camelCase (e.g., "fee_rate" -> "feeRate").
pub fn to_camel_case(s: &str) -> String {
let pascal = to_pascal_case(s);
let mut chars = pascal.chars();
let result = match chars.next() {
None => String::new(),
Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
};
// Prefix with _ if starts with digit
if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
format!("_{}", result)
} else {
result
}
}
/// Convert an Index to a snake_case field name (e.g., DateIndex -> dateindex).
pub fn index_to_field_name(index: &Index) -> String {
to_snake_case(index.serialize_long())
}
/// Generate a child type/struct/class name (e.g., ParentName + child_name -> ParentName_ChildName).
pub fn child_type_name(parent: &str, child: &str) -> String {
format!("{}_{}", parent, to_pascal_case(child))
}
/// Escape Python reserved keywords by appending an underscore.
/// Also prefixes names starting with digits with an underscore.
pub fn escape_python_keyword(name: &str) -> String {
const PYTHON_KEYWORDS: &[&str] = &[
"False", "None", "True", "and", "as", "assert", "async", "await", "break", "class",
"continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global",
"if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return",
"try", "while", "with", "yield",
];
// Prefix with underscore if starts with digit
let name = if name.starts_with(|c: char| c.is_ascii_digit()) {
format!("_{}", name)
} else {
name.to_string()
};
// Append underscore if it's a keyword
if PYTHON_KEYWORDS.contains(&name.as_str()) {
format!("{}_", name)
} else {
name
}
}
+199
View File
@@ -0,0 +1,199 @@
//! Client metadata extracted from brk_query.
use std::collections::{BTreeSet, HashMap};
use brk_query::Vecs;
use brk_types::{Index, MetricLeafWithSchema};
use super::{GenericSyntax, IndexSetPattern, PatternField, StructuralPattern, extract_inner_type};
use crate::{PatternBaseResult, analysis};
/// Metadata extracted from brk_query for client generation.
#[derive(Debug)]
pub struct ClientMetadata {
/// The catalog tree structure (with schemas in leaves)
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
pub index_set_patterns: Vec<IndexSetPattern>,
/// Maps concrete field signatures to pattern names
concrete_to_pattern: HashMap<Vec<PatternField>, String>,
/// Maps concrete field signatures to their type parameter (for generic patterns)
concrete_to_type_param: HashMap<Vec<PatternField>, String>,
/// Maps tree paths to their computed PatternBaseResult
node_bases: HashMap<String, PatternBaseResult>,
}
impl ClientMetadata {
/// Extract metadata from brk_query::Vecs.
pub fn from_vecs(vecs: &Vecs) -> Self {
Self::from_catalog(vecs.catalog().clone())
}
/// Extract metadata from a catalog TreeNode directly.
pub fn from_catalog(catalog: brk_types::TreeNode) -> Self {
let (structural_patterns, concrete_to_pattern, concrete_to_type_param, node_bases) =
analysis::detect_structural_patterns(&catalog);
let index_set_patterns = analysis::detect_index_patterns(&catalog);
ClientMetadata {
catalog,
structural_patterns,
index_set_patterns,
concrete_to_pattern,
concrete_to_type_param,
node_bases,
}
}
/// Find an index set pattern that matches the given indexes.
pub fn find_index_set_pattern(&self, indexes: &BTreeSet<Index>) -> Option<&IndexSetPattern> {
self.index_set_patterns
.iter()
.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).
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
}
})
})
}
/// 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.
pub fn find_pattern_by_fields(&self, fields: &[PatternField]) -> Option<&StructuralPattern> {
self.concrete_to_pattern
.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.
pub fn get_type_param(&self, fields: &[PatternField]) -> Option<&String> {
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 PatternBaseResult for a tree path.
pub fn get_node_base(&self, path: &str) -> Option<&PatternBaseResult> {
self.node_bases.get(path)
}
/// Generate type annotation for a field with language-specific syntax.
pub fn field_type_annotation(
&self,
field: &PatternField,
is_generic: bool,
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) {
let type_param = field
.type_param
.as_deref()
.or(generic_value_type)
.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) {
syntax.wrap(&accessor.name, &value_type)
} else {
syntax.wrap("MetricNode", &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`.
pub fn field_type_annotation_from_leaf(
&self,
leaf: &MetricLeafWithSchema,
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)
}
}
}
+68
View File
@@ -0,0 +1,68 @@
//! Core types for client generation.
mod case;
mod metadata;
mod positions;
mod schema;
mod structs;
pub use case::*;
pub use metadata::*;
pub use positions::*;
pub use schema::*;
pub use structs::*;
/// Language-specific syntax for generic type annotations.
#[derive(Clone, Copy)]
pub struct GenericSyntax {
pub open: char,
pub close: char,
pub default_type: &'static str,
}
impl GenericSyntax {
pub const PYTHON: Self = Self {
open: '[',
close: ']',
default_type: "Any",
};
pub const JAVASCRIPT: Self = Self {
open: '<',
close: '>',
default_type: "unknown",
};
pub const RUST: Self = Self {
open: '<',
close: '>',
default_type: "_",
};
pub fn wrap(&self, name: &str, type_param: &str) -> String {
// Convert the type_param from Rust syntax to target syntax
let converted = self.convert(type_param);
format!("{}{}{}{}", name, self.open, converted, self.close)
}
/// Convert a type string from Rust generic syntax to target language syntax.
///
/// For Python, wrapper newtypes like `Close<Cents>` are flattened to just `Cents`
/// because Python type aliases can't be parameterized. This matches JS behavior.
pub fn convert(&self, type_str: &str) -> String {
// Flatten nested generics to innermost type (e.g., Close<Cents> -> Cents)
// This is needed because wrapper types like Close, Open, High, Low are
// just type aliases in generated code, not actual generic classes.
extract_inner_type_recursive(type_str)
}
}
/// Extract the innermost type from nested generics.
/// E.g., `Close<Cents>` -> `Cents`, `Foo<Bar<Baz>>` -> `Baz`
fn extract_inner_type_recursive(type_str: &str) -> String {
if let Some(start) = type_str.find('<')
&& let Some(end) = type_str.rfind('>')
{
let inner = &type_str[start + 1..end];
return extract_inner_type_recursive(inner);
}
type_str.to_string()
}
+26
View File
@@ -0,0 +1,26 @@
//! Pattern mode and field parts for metric 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;
/// How a pattern constructs metric 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>,
},
/// 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>,
},
}
+43
View File
@@ -0,0 +1,43 @@
use serde_json::Value;
/// Unwrap allOf with a single element, returning the inner schema.
/// Schemars uses allOf for composition, but often with just one $ref.
pub fn unwrap_allof(schema: &Value) -> &Value {
if let Some(all_of) = schema.get("allOf").and_then(|v| v.as_array())
&& all_of.len() == 1
{
return &all_of[0];
}
schema
}
/// Extract inner type from a wrapper generic like `Close<Dollars>` -> `Dollars`.
/// Also handles malformed types like `Dollars>` (from vecdb's short_type_name).
pub fn extract_inner_type(type_str: &str) -> String {
// Handle proper generic wrappers like `Close<Dollars>` -> `Dollars`
if let Some(start) = type_str.find('<')
&& let Some(end) = type_str.rfind('>')
&& start < end
{
return type_str[start + 1..end].to_string();
}
// Handle malformed types like `Dollars>` (trailing > without <)
if type_str.ends_with('>') && !type_str.contains('<') {
return type_str.trim_end_matches('>').to_string();
}
type_str.to_string()
}
/// Extract type name from a JSON Schema $ref path.
/// E.g., "#/definitions/MyType" -> "MyType", "#/$defs/Foo" -> "Foo"
pub fn ref_to_type_name(ref_path: &str) -> Option<&str> {
ref_path.rsplit('/').next()
}
/// Get union variants from anyOf or oneOf schema.
pub fn get_union_variants(schema: &Value) -> Option<&Vec<Value>> {
schema
.get("anyOf")
.or_else(|| schema.get("oneOf"))
.and_then(|v| v.as_array())
}
+121
View File
@@ -0,0 +1,121 @@
//! Structural pattern and field types.
use std::collections::{BTreeSet, HashMap};
use brk_types::Index;
use super::PatternMode;
/// A pattern of indexes that appear together on multiple metrics.
#[derive(Debug, Clone)]
pub struct IndexSetPattern {
/// Pattern name (e.g., "DateHeightIndexes")
pub name: String,
/// The set of indexes
pub indexes: BTreeSet<Index>,
}
/// A structural pattern - a branch structure that appears multiple times.
#[derive(Debug, Clone)]
pub struct StructuralPattern {
/// Pattern name
pub name: String,
/// Ordered list of child fields
pub fields: Vec<PatternField>,
/// How fields construct metric names from acc (None = not parameterizable)
pub mode: Option<PatternMode>,
/// If true, all leaf fields use a type parameter T
pub is_generic: bool,
}
impl StructuralPattern {
/// Returns true if this pattern can be parameterized with an accumulator.
pub fn is_parameterizable(&self) -> bool {
self.mode.is_some()
}
/// 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::Prefix { prefixes }) => prefixes.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 { .. }))
}
/// 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 {
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)
.is_some_and(|instance_suffix| instance_suffix == pattern_suffix)
})
}
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
}
}
}
/// A field in a structural pattern.
#[derive(Debug, Clone, PartialOrd, Ord)]
pub struct PatternField {
/// Field name
pub name: String,
/// Rust type for leaves or pattern name for branches
pub rust_type: String,
/// JSON type from schema
pub json_type: String,
/// For leaves: the set of supported indexes. Empty for branches.
pub indexes: BTreeSet<Index>,
/// For branches referencing generic patterns: the concrete type parameter
pub type_param: Option<String>,
}
impl PatternField {
/// Returns true if this is a leaf field (has indexes).
pub fn is_leaf(&self) -> bool {
!self.indexes.is_empty()
}
/// Returns true if this is a branch field (no indexes).
pub fn is_branch(&self) -> bool {
self.indexes.is_empty()
}
}
impl std::hash::Hash for PatternField {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
self.rust_type.hash(state);
self.json_type.hash(state);
self.indexes.hash(state);
}
}
impl PartialEq for PatternField {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
&& self.rust_type == other.rust_type
&& self.json_type == other.json_type
&& self.indexes == other.indexes
}
}
impl Eq for PatternField {}
+913
View File
@@ -0,0 +1,913 @@
//! Tests that verify pattern analysis using the real catalog.
use std::collections::HashSet;
use std::fmt::Write;
use brk_bindgen::ClientMetadata;
use brk_types::TreeNode;
/// Load the catalog from the JSON file.
fn load_catalog() -> TreeNode {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/catalog.json");
let catalog_json = std::fs::read_to_string(path).expect("Failed to read catalog.json");
serde_json::from_str(&catalog_json).expect("Failed to parse catalog.json")
}
/// Load OpenAPI spec from openapi.json.
fn load_openapi_json() -> String {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/openapi.json");
std::fs::read_to_string(path).expect("Failed to read openapi.json")
}
/// Load metadata from the catalog.
#[allow(unused)]
fn load_metadata() -> ClientMetadata {
ClientMetadata::from_catalog(load_catalog())
}
/// Collect all leaf metric names from a tree.
fn collect_leaf_names(node: &TreeNode, names: &mut HashSet<String>) {
match node {
TreeNode::Leaf(leaf) => {
names.insert(leaf.name().to_string());
}
TreeNode::Branch(children) => {
for child in children.values() {
collect_leaf_names(child, names);
}
}
}
}
#[test]
fn test_catalog_loads() {
let catalog = load_catalog();
// Should be a branch with top-level categories
let TreeNode::Branch(categories) = &catalog else {
panic!("Expected catalog to be a branch");
};
// Check some expected top-level categories exist
assert!(
categories.contains_key("addresses"),
"Missing addresses category"
);
assert!(categories.contains_key("blocks"), "Missing blocks category");
assert!(categories.contains_key("market"), "Missing market category");
assert!(categories.contains_key("supply"), "Missing supply category");
println!("Catalog has {} top-level categories", categories.len());
}
#[test]
fn test_all_leaves_have_names() {
let catalog = load_catalog();
let mut names = HashSet::new();
collect_leaf_names(&catalog, &mut names);
println!("Catalog has {} unique metric names", names.len());
assert!(!names.is_empty(), "Should have at least some metrics");
// All names should be non-empty
for name in &names {
assert!(!name.is_empty(), "Found empty metric name");
}
}
#[test]
fn test_pattern_detection() {
let catalog = load_catalog();
let (patterns, concrete_to_pattern, concrete_to_type_param, _node_bases) =
brk_bindgen::detect_structural_patterns(&catalog);
println!("Detected {} structural patterns", patterns.len());
println!(
"Concrete to pattern mappings: {}",
concrete_to_pattern.len()
);
println!("Type parameter mappings: {}", concrete_to_type_param.len());
// Print pattern details
for pattern in &patterns {
let mode_str = match &pattern.mode {
Some(brk_bindgen::PatternMode::Suffix { relatives }) => {
format!("Suffix({})", relatives.len())
}
Some(brk_bindgen::PatternMode::Prefix { prefixes }) => {
format!("Prefix({})", prefixes.len())
}
None => "None".to_string(),
};
println!(
" {} (fields: {}, generic: {}, mode: {})",
pattern.name,
pattern.fields.len(),
pattern.is_generic,
mode_str
);
}
// Should have detected some patterns
assert!(!patterns.is_empty(), "Should detect at least some patterns");
// Check that parameterizable patterns have valid modes
for pattern in &patterns {
if pattern.is_parameterizable() {
let mode = pattern.mode.as_ref().unwrap();
match mode {
brk_bindgen::PatternMode::Suffix { relatives } => {
assert_eq!(
relatives.len(),
pattern.fields.len(),
"Pattern {} should have relative for each field",
pattern.name
);
}
brk_bindgen::PatternMode::Prefix { prefixes } => {
assert_eq!(
prefixes.len(),
pattern.fields.len(),
"Pattern {} should have prefix for each field",
pattern.name
);
}
}
}
}
}
#[test]
fn test_cost_basis_pattern() {
let catalog = load_catalog();
let (patterns, _, _, _) = brk_bindgen::detect_structural_patterns(&catalog);
// Find CostBasisPattern2 and inspect it
let cost_basis = patterns
.iter()
.find(|p| p.name == "CostBasisPattern2")
.expect("CostBasisPattern2 should exist");
println!("CostBasisPattern2:");
println!(
" Fields: {:?}",
cost_basis
.fields
.iter()
.map(|f| &f.name)
.collect::<Vec<_>>()
);
println!(" Mode: {:?}", cost_basis.mode);
println!(" Is generic: {}", cost_basis.is_generic);
// With suffix naming convention (cost_basis_max, cost_basis_min, cost_basis):
//
// At root level: common prefix is "cost_basis_" -> suffix mode
// max -> "max"
// min -> "min"
// percentiles -> "" (identity)
//
// At lth_ level: common prefix is "lth_cost_basis_" -> suffix mode
// max -> "max"
// min -> "min"
// percentiles -> "" (identity)
//
// Both use suffix mode with same relatives, so pattern IS parameterizable!
assert!(
cost_basis.is_parameterizable(),
"CostBasisPattern2 should be parameterizable with consistent suffix mode"
);
}
#[test]
fn test_realized_pattern3_fields() {
let catalog = load_catalog();
let metadata = ClientMetadata::from_catalog(catalog);
let pattern = metadata
.find_pattern("RealizedPattern3")
.expect("RealizedPattern3 should exist");
println!("RealizedPattern3 fields:");
for field in &pattern.fields {
let is_branch = field.is_branch();
let is_pattern = metadata.find_pattern(&field.rust_type).is_some();
let is_param = metadata.is_parameterizable(&field.rust_type);
println!(
" {} -> {} (branch={}, pattern={}, param={})",
field.name, field.rust_type, is_branch, is_pattern, is_param
);
}
// Check if RealizedPattern3 is considered parameterizable
println!(
"\nRealizedPattern3 is_parameterizable (metadata): {}",
metadata.is_parameterizable("RealizedPattern3")
);
}
#[test]
fn test_parameterizable_patterns_have_mode() {
let catalog = load_catalog();
let (patterns, _, _, _) = brk_bindgen::detect_structural_patterns(&catalog);
// All patterns that appear 2+ times should either:
// 1. Be parameterizable (have a mode)
// 2. Or have inconsistent instances (mode = None)
//
// Patterns with mode = None should be inlined, not generate factories
let parameterizable: Vec<_> = patterns.iter().filter(|p| p.is_parameterizable()).collect();
let non_parameterizable: Vec<_> = patterns
.iter()
.filter(|p| !p.is_parameterizable())
.collect();
println!("\nParameterizable patterns ({}):", parameterizable.len());
for p in &parameterizable {
let mode = p.mode.as_ref().unwrap();
let mode_type = match mode {
brk_bindgen::PatternMode::Suffix { .. } => "Suffix",
brk_bindgen::PatternMode::Prefix { .. } => "Prefix",
};
println!(" {} ({} fields, {})", p.name, p.fields.len(), mode_type);
}
println!(
"\nNon-parameterizable patterns ({}):",
non_parameterizable.len()
);
for p in &non_parameterizable {
println!(" {} ({} fields)", p.name, p.fields.len());
}
// Verify all parameterizable patterns have valid modes with all fields
for pattern in &parameterizable {
let mode = pattern.mode.as_ref().unwrap();
let field_names: HashSet<_> = pattern.fields.iter().map(|f| f.name.clone()).collect();
match mode {
brk_bindgen::PatternMode::Suffix { relatives } => {
let mode_fields: HashSet<_> = relatives.keys().cloned().collect();
assert_eq!(
field_names, mode_fields,
"Pattern {} suffix mode should have all fields",
pattern.name
);
}
brk_bindgen::PatternMode::Prefix { prefixes } => {
let mode_fields: HashSet<_> = prefixes.keys().cloned().collect();
assert_eq!(
field_names, mode_fields,
"Pattern {} prefix mode should have all fields",
pattern.name
);
}
}
}
}
#[test]
fn test_fee_rate_pattern_relatives() {
let catalog = load_catalog();
let (patterns, _, _, _) = brk_bindgen::detect_structural_patterns(&catalog);
let fee_rate_pattern = patterns
.iter()
.find(|p| p.name == "FeeRatePattern")
.expect("FeeRatePattern should exist");
println!("FeeRatePattern mode:");
if let Some(mode) = &fee_rate_pattern.mode {
match mode {
brk_bindgen::PatternMode::Suffix { relatives } => {
println!(" Suffix mode:");
for (field, relative) in relatives {
println!(" {} -> '{}'", field, relative);
}
}
brk_bindgen::PatternMode::Prefix { prefixes } => {
println!(" Prefix mode:");
for (field, prefix) in prefixes {
println!(" {} -> '{}'", field, prefix);
}
}
}
} else {
println!(" No mode (not parameterizable)");
}
// Check that relatives are correct - should be "average", "max", etc.
// NOT "tx_weight_average", "tx_weight_max", etc.
if let Some(brk_bindgen::PatternMode::Suffix { relatives }) = &fee_rate_pattern.mode {
assert_eq!(
relatives.get("average"),
Some(&"average".to_string()),
"average relative should be 'average', not 'tx_weight_average'"
);
}
}
#[test]
fn test_index_patterns() {
let catalog = load_catalog();
let index_patterns = brk_bindgen::detect_index_patterns(&catalog);
// println!("Used indexes: {:?}", used_indexes);
println!("Index set patterns: {}", index_patterns.len());
for pattern in &index_patterns {
println!(" {} -> {:?}", pattern.name, pattern.indexes);
}
// Should have detected some index patterns
assert!(!index_patterns.is_empty(), "Should detect index patterns");
}
#[test]
fn test_generated_rust_output() {
let catalog = load_catalog();
let metadata = ClientMetadata::from_catalog(catalog.clone());
// Collect all metric names from the catalog
let mut all_metrics = HashSet::new();
collect_leaf_names(&catalog, &mut all_metrics);
// Generate Rust client output
let mut rust_output = String::new();
brk_bindgen::rust::client::generate_imports(&mut rust_output);
brk_bindgen::rust::client::generate_base_client(&mut rust_output);
brk_bindgen::rust::client::generate_metric_pattern_trait(&mut rust_output);
brk_bindgen::rust::client::generate_endpoint(&mut rust_output);
brk_bindgen::rust::client::generate_index_accessors(
&mut rust_output,
&metadata.index_set_patterns,
);
brk_bindgen::rust::client::generate_pattern_structs(
&mut rust_output,
&metadata.structural_patterns,
&metadata,
);
brk_bindgen::rust::tree::generate_tree(&mut rust_output, &metadata.catalog, &metadata);
brk_bindgen::rust::api::generate_main_client(&mut rust_output, &[]);
// Count metrics that appear as direct string literals
let mut direct_metrics = 0;
for metric in &all_metrics {
if rust_output.contains(&format!("\"{}\"", metric)) {
direct_metrics += 1;
}
}
println!("\nGenerated Rust output stats:");
println!(" Total metrics in catalog: {}", all_metrics.len());
println!(" Direct string literals: {}", direct_metrics);
println!(
" Via pattern factories: {}",
all_metrics.len() - direct_metrics
);
println!(" Output size: {} bytes", rust_output.len());
// Write output to test directory (not actual client)
let output_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/output");
std::fs::create_dir_all(output_dir).ok();
let output_path = format!("{}/rust_client.rs", output_dir);
std::fs::write(&output_path, &rust_output).expect("Failed to write client output");
println!(" Wrote output to: {}", output_path);
// Verify the output contains the key components
assert!(rust_output.contains("fn _m("), "Should define _m helper");
assert!(
rust_output.contains("pub struct MetricsTree"),
"Should have MetricsTree"
);
assert!(
rust_output.contains("impl MetricsTree"),
"Should have MetricsTree impl"
);
// Count parameterizable patterns (these use _m for dynamic metric names)
// Use metadata.is_parameterizable() for full recursive check
let parameterizable_count = metadata
.structural_patterns
.iter()
.filter(|p| metadata.is_parameterizable(&p.name))
.count();
println!(" Parameterizable patterns: {}", parameterizable_count);
// Verify all pattern structs are generated (parameterizable and non)
for pattern in &metadata.structural_patterns {
assert!(
rust_output.contains(&format!("pub struct {}", pattern.name)),
"Missing pattern struct: {}",
pattern.name
);
}
println!("\nGenerated Rust client is complete!");
}
#[test]
fn test_generated_javascript_output() {
let catalog = load_catalog();
let metadata = ClientMetadata::from_catalog(catalog.clone());
// Collect all metric names from the catalog
let mut all_metrics = HashSet::new();
collect_leaf_names(&catalog, &mut all_metrics);
// Load schemas from OpenAPI spec only (catalog schemas require runtime data)
let openapi_json = load_openapi_json();
let schemas = brk_bindgen::extract_schemas(&openapi_json);
// Generate JavaScript client output
let mut js_output = String::new();
writeln!(js_output, "// Auto-generated BRK JavaScript client").unwrap();
writeln!(js_output, "// Do not edit manually\n").unwrap();
brk_bindgen::javascript::types::generate_type_definitions(&mut js_output, &schemas);
brk_bindgen::javascript::client::generate_base_client(&mut js_output);
brk_bindgen::javascript::client::generate_index_accessors(
&mut js_output,
&metadata.index_set_patterns,
);
brk_bindgen::javascript::client::generate_structural_patterns(
&mut js_output,
&metadata.structural_patterns,
&metadata,
);
brk_bindgen::javascript::tree::generate_tree_typedefs(
&mut js_output,
&metadata.catalog,
&metadata,
);
brk_bindgen::javascript::tree::generate_main_client(
&mut js_output,
&metadata.catalog,
&metadata,
&[],
);
// Count metrics that appear as direct string literals
let mut direct_metrics = 0;
for metric in &all_metrics {
if js_output.contains(&format!("'{}'", metric))
|| js_output.contains(&format!("\"{}\"", metric))
{
direct_metrics += 1;
}
}
println!("\nGenerated JavaScript output stats:");
println!(" Total metrics in catalog: {}", all_metrics.len());
println!(" Direct string literals: {}", direct_metrics);
println!(
" Via pattern factories: {}",
all_metrics.len() - direct_metrics
);
println!(" Output size: {} bytes", js_output.len());
println!(" Output lines: {}", js_output.lines().count());
// Write output to test directory (not actual client)
let output_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/output");
std::fs::create_dir_all(output_dir).ok();
let output_path = format!("{}/js_client.js", output_dir);
std::fs::write(&output_path, &js_output).expect("Failed to write JS client output");
println!(" Wrote output to: {}", output_path);
// Verify the output contains key components
assert!(js_output.contains("const _m ="), "Should define _m helper");
assert!(js_output.contains("const _p ="), "Should define _p helper");
assert!(
js_output.contains("@typedef {Object} MetricsTree"),
"Should have MetricsTree typedef"
);
assert!(
js_output.contains("class BrkClient"),
"Should have BrkClient class"
);
// Verify all pattern factories are generated
for pattern in &metadata.structural_patterns {
assert!(
js_output.contains(&format!("function create{}(", pattern.name)),
"Missing pattern factory: {}",
pattern.name
);
}
println!("\nGenerated JavaScript client is complete!");
}
#[test]
fn test_generated_python_output() {
let catalog = load_catalog();
let metadata = ClientMetadata::from_catalog(catalog.clone());
// Collect all metric names from the catalog
let mut all_metrics = HashSet::new();
collect_leaf_names(&catalog, &mut all_metrics);
// Load schemas from OpenAPI spec only (catalog schemas require runtime data)
let openapi_json = load_openapi_json();
let schemas = brk_bindgen::extract_schemas(&openapi_json);
// Generate Python client output
let mut py_output = String::new();
writeln!(py_output, "# Auto-generated BRK Python client").unwrap();
writeln!(py_output, "# Do not edit manually\n").unwrap();
writeln!(py_output, "from typing import TypeVar, Generic, Any, Optional, List, Literal, TypedDict, Union, Protocol, overload").unwrap();
writeln!(
py_output,
"from http.client import HTTPSConnection, HTTPConnection"
)
.unwrap();
writeln!(py_output, "from urllib.parse import urlparse").unwrap();
writeln!(py_output, "import json\n").unwrap();
writeln!(py_output, "T = TypeVar('T')\n").unwrap();
brk_bindgen::python::types::generate_type_definitions(&mut py_output, &schemas);
brk_bindgen::python::client::generate_base_client(&mut py_output);
brk_bindgen::python::client::generate_endpoint_class(&mut py_output);
brk_bindgen::python::client::generate_index_accessors(
&mut py_output,
&metadata.index_set_patterns,
);
brk_bindgen::python::client::generate_structural_patterns(
&mut py_output,
&metadata.structural_patterns,
&metadata,
);
brk_bindgen::python::tree::generate_tree_classes(&mut py_output, &metadata.catalog, &metadata);
brk_bindgen::python::api::generate_main_client(&mut py_output, &[]);
// Count metrics that appear as direct string literals
let mut direct_metrics = 0;
for metric in &all_metrics {
if py_output.contains(&format!("'{}'", metric))
|| py_output.contains(&format!("\"{}\"", metric))
{
direct_metrics += 1;
}
}
println!("\nGenerated Python output stats:");
println!(" Total metrics in catalog: {}", all_metrics.len());
println!(" Direct string literals: {}", direct_metrics);
println!(
" Via pattern factories: {}",
all_metrics.len() - direct_metrics
);
println!(" Output size: {} bytes", py_output.len());
println!(" Output lines: {}", py_output.lines().count());
// Write output to test directory (not actual client)
let output_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/output");
std::fs::create_dir_all(output_dir).ok();
let output_path = format!("{}/python_client.py", output_dir);
std::fs::write(&output_path, &py_output).expect("Failed to write Python client output");
println!(" Wrote output to: {}", output_path);
// Verify the output contains key components
assert!(py_output.contains("def _m("), "Should define _m helper");
assert!(py_output.contains("def _p("), "Should define _p helper");
assert!(
py_output.contains("class MetricsTree:"),
"Should have MetricsTree class"
);
assert!(
py_output.contains("class BrkClient"),
"Should have BrkClient class"
);
// Verify all pattern classes have constructors
for pattern in &metadata.structural_patterns {
assert!(
py_output.contains(&format!("class {}:", pattern.name))
|| py_output.contains(&format!("class {}(", pattern.name)),
"Missing pattern class: {}",
pattern.name
);
}
println!("\nGenerated Python client is complete!");
}
#[test]
fn test_cost_basis_relatives() {
let catalog = load_catalog();
// Find cost_basis branches that have 3 direct children (max, min, percentiles)
fn find_cost_basis_with_percentiles(
node: &TreeNode,
path: &str,
) -> Vec<(String, Vec<(String, String)>)> {
let mut results = Vec::new();
if let TreeNode::Branch(children) = node {
for (name, child) in children {
let child_path = if path.is_empty() {
name.clone()
} else {
format!("{}.{}", path, name)
};
if name == "cost_basis"
&& let TreeNode::Branch(cb_children) = child
&& cb_children.contains_key("percentiles")
{
// Found a cost_basis with percentiles
let mut metrics = Vec::new();
for (field_name, field_node) in cb_children {
match field_node {
TreeNode::Leaf(leaf) => {
metrics.push((field_name.clone(), leaf.name().to_string()));
}
TreeNode::Branch(pct_children) => {
// Get first percentile as example
if let Some((_, TreeNode::Leaf(first))) = pct_children.iter().next()
{
metrics.push((
format!("{}.first", field_name),
first.name().to_string(),
));
}
}
}
}
results.push((child_path.clone(), metrics));
}
results.extend(find_cost_basis_with_percentiles(child, &child_path));
}
}
results
}
let instances = find_cost_basis_with_percentiles(&catalog, "");
println!("\nCostBasisPattern2 instances (with percentiles):");
for (path, metrics) in instances.iter().take(10) {
println!(" {}:", path);
for (field, metric) in metrics {
println!(" {} -> {}", field, metric);
}
}
// Now compute what relatives the pattern detection would see
// The key is: percentiles returns its BASE (common prefix of pct05, pct10, etc.)
// not the individual percentile metrics
use brk_bindgen::find_common_prefix;
println!("\nComputing relatives (simulating branch base returns):");
for (path, metrics) in instances.iter().take(5) {
println!(" Instance: {}", path);
// For leaves (max, min), the base is the metric name
// For branches (percentiles), the base is the common prefix of its children
let mut child_bases: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
for (field, metric) in metrics {
if field.starts_with("percentiles.") {
// This is a percentile metric - compute what the percentiles branch would return
// The base is the metric name with the pct suffix stripped
let base = metric
.strip_suffix("_pct05")
.or_else(|| metric.strip_suffix("_pct10"))
.unwrap_or(metric)
.to_string();
child_bases.insert("percentiles".to_string(), base);
} else {
child_bases.insert(field.clone(), metric.clone());
}
}
let bases: Vec<&str> = child_bases.values().map(|s| s.as_str()).collect();
println!(" Child bases:");
for (field, base) in &child_bases {
println!(" {} -> {}", field, base);
}
if let Some(prefix) = find_common_prefix(&bases) {
println!(" Common prefix: '{}'", prefix);
for (field, base) in &child_bases {
let relative = base.strip_prefix(&prefix).unwrap_or(base);
println!(" {} -> relative '{}'", field, relative);
}
} else {
println!(" No common prefix found!");
}
}
}
#[test]
fn test_debug_cost_basis_pattern2_mode() {
// Debug why CostBasisPattern2 has mode=None
let catalog = load_catalog();
let metadata = brk_bindgen::ClientMetadata::from_catalog(catalog.clone());
let pattern_lookup = metadata.pattern_lookup();
let pattern = metadata
.find_pattern("CostBasisPattern2")
.expect("CostBasisPattern2 should exist");
println!("\nCostBasisPattern2 fields:");
for field in &pattern.fields {
println!(" {} (type: {})", field.name, field.rust_type);
}
println!("Mode: {:?}", pattern.mode);
// Now debug the instance collection
#[derive(Debug, Clone)]
struct DebugInstanceAnalysis {
base: String,
field_parts: std::collections::HashMap<String, String>,
is_suffix_mode: bool,
}
fn collect_debug(
node: &TreeNode,
pattern_lookup: &std::collections::HashMap<Vec<brk_bindgen::PatternField>, String>,
all_analyses: &mut std::collections::HashMap<String, Vec<DebugInstanceAnalysis>>,
) -> Option<String> {
match node {
TreeNode::Leaf(leaf) => Some(leaf.name().to_string()),
TreeNode::Branch(children) => {
let mut child_bases: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
for (field_name, child_node) in children {
if let Some(base) = collect_debug(child_node, pattern_lookup, all_analyses) {
child_bases.insert(field_name.clone(), base);
}
}
if child_bases.is_empty() {
return None;
}
// Analyze this instance
let bases: Vec<&str> = child_bases.values().map(|s| s.as_str()).collect();
let (base, field_parts, is_suffix_mode) =
if let Some(common_prefix) = brk_bindgen::find_common_prefix(&bases) {
let base = common_prefix.trim_end_matches('_').to_string();
let mut parts = std::collections::HashMap::new();
for (field_name, child_base) in &child_bases {
let relative = if *child_base == base {
String::new()
} else {
child_base
.strip_prefix(&common_prefix)
.unwrap_or(child_base)
.to_string()
};
parts.insert(field_name.clone(), relative);
}
(base, parts, true)
} else {
let base = child_bases.values().next().cloned().unwrap_or_default();
let parts = child_bases
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
(base, parts, true)
};
let analysis = DebugInstanceAnalysis {
base: base.clone(),
field_parts,
is_suffix_mode,
};
// Get the pattern name for this node
let fields = brk_bindgen::get_node_fields(children, pattern_lookup);
if let Some(pattern_name) = pattern_lookup.get(&fields) {
all_analyses
.entry(pattern_name.clone())
.or_default()
.push(analysis);
}
Some(base)
}
}
}
let mut all_analyses: std::collections::HashMap<String, Vec<DebugInstanceAnalysis>> =
std::collections::HashMap::new();
collect_debug(&catalog, &pattern_lookup, &mut all_analyses);
if let Some(analyses) = all_analyses.get("CostBasisPattern2") {
println!(
"\nCollected {} instances of CostBasisPattern2:",
analyses.len()
);
for (i, a) in analyses.iter().enumerate() {
println!(" Instance {}:", i);
println!(" base: {}", a.base);
println!(" is_suffix: {}", a.is_suffix_mode);
println!(" field_parts:");
for (f, p) in &a.field_parts {
println!(" {} -> '{}'", f, p);
}
}
// Check consistency
if analyses.len() >= 2 {
let first = &analyses[0];
for (i, a) in analyses.iter().enumerate().skip(1) {
if a.is_suffix_mode != first.is_suffix_mode {
println!(" INCONSISTENT: Instance {} has different mode", i);
}
for (field, part) in &a.field_parts {
if first.field_parts.get(field) != Some(part) {
println!(
" INCONSISTENT: Instance {} field '{}' has part '{}' vs '{}'",
i,
field,
part,
first
.field_parts
.get(field)
.unwrap_or(&"<missing>".to_string())
);
}
}
}
}
} else {
println!("\nNo instances collected for CostBasisPattern2!");
}
}
#[test]
fn test_root_cost_basis_prefix() {
use brk_bindgen::find_common_prefix;
// Root-level cost_basis has:
// max -> "max_cost_basis"
// min -> "min_cost_basis"
// percentiles -> "cost_basis" (base of pct05, pct10, etc.)
let bases = vec!["max_cost_basis", "min_cost_basis", "cost_basis"];
let prefix = find_common_prefix(&bases);
println!("Root cost_basis prefix: {:?}", prefix);
// Compare with nested cost_basis
let nested_bases = vec![
"utxos_at_least_15y_old_max_cost_basis",
"utxos_at_least_15y_old_min_cost_basis",
"utxos_at_least_15y_old_cost_basis",
];
let nested_prefix = find_common_prefix(&nested_bases);
println!("Nested cost_basis prefix: {:?}", nested_prefix);
}
#[test]
fn test_utxo_cohorts_all_activity_base() {
// Test that distribution.utxo_cohorts.all.activity uses empty base
// because its children (coinblocks_destroyed, coindays_destroyed, etc.)
// have no common prefix or suffix.
let catalog = load_catalog();
let metadata = ClientMetadata::from_catalog(catalog.clone());
// Generate JavaScript output
let mut js_output = String::new();
writeln!(js_output, "// Test output").unwrap();
brk_bindgen::javascript::client::generate_base_client(&mut js_output);
brk_bindgen::javascript::client::generate_index_accessors(
&mut js_output,
&metadata.index_set_patterns,
);
brk_bindgen::javascript::client::generate_structural_patterns(
&mut js_output,
&metadata.structural_patterns,
&metadata,
);
brk_bindgen::javascript::tree::generate_tree_typedefs(
&mut js_output,
&metadata.catalog,
&metadata,
);
brk_bindgen::javascript::tree::generate_main_client(
&mut js_output,
&metadata.catalog,
&metadata,
&[],
);
// The all.activity should use empty base, so metrics don't get duplicated
// Look for: activity: createActivityPattern2(this, '')
// NOT: activity: createActivityPattern2(this, 'coinblocks_destroyed')
assert!(
!js_output.contains("createActivityPattern2(this, 'coinblocks_destroyed')"),
"all.activity should NOT use 'coinblocks_destroyed' as base (causes duplication)"
);
// Check that it uses empty string as base
assert!(
js_output.contains("activity: createActivityPattern2(this, '')"),
"all.activity should use empty base"
);
println!("utxo_cohorts.all.activity base test passed!");
}
+37
View File
@@ -0,0 +1,37 @@
[package]
name = "brk_cli"
description = "A command line interface to run a BRK instance"
version.workspace = true
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
[dependencies]
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 }
brk_reader = { workspace = true }
brk_rpc = { workspace = true }
brk_server = { workspace = true }
brk_types = { workspace = true }
lexopt = "0.3"
tracing = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
toml = "0.9.11"
vecdb = { workspace = true }
[[bin]]
name = "brk"
path = "src/main.rs"
[package.metadata.dist]
dist = true
+55
View File
@@ -0,0 +1,55 @@
# brk_cli
Command-line interface for running a Bitcoin Research Kit instance.
## Preview
- https://bitview.space - web interface
- https://bitview.space/api - API docs
## Requirements
- Bitcoin Core running with RPC enabled
- Access to `blk*.dat` files
- ~400 GB disk space
- 12+ GB RAM
## 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}')"
```
Portable build (without native CPU optimizations):
```bash
cargo install --locked brk_cli
```
## Run
```bash
brk
```
Indexes the blockchain, computes datasets, starts the server on `localhost:3110`, and waits for new blocks.
## Options
```bash
brk -h # Show all options
brk -V # Show version
```
Options are saved to `~/.brk/config.toml` after first use.
## Files
```
~/.brk/
├── config.toml Configuration
└── log Logs
<brkdir>/ Indexed data (default: ~/.brk)
```
+314
View File
@@ -0,0 +1,314 @@
use std::{
fs,
path::{Path, PathBuf},
};
use brk_error::{Error, Result};
use brk_fetcher::Fetcher;
use brk_rpc::{Auth, Client};
use brk_types::Port;
use serde::{Deserialize, Deserializer, Serialize};
use crate::{default_brk_path, dot_brk_path, fix_user_path, website::WebsiteArg};
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
pub struct Config {
#[serde(default, deserialize_with = "default_on_error")]
brkdir: Option<String>,
#[serde(default, deserialize_with = "default_on_error")]
brkport: Option<Port>,
#[serde(default, deserialize_with = "default_on_error")]
website: Option<WebsiteArg>,
#[serde(default, deserialize_with = "default_on_error")]
fetch: Option<bool>,
#[serde(default, deserialize_with = "default_on_error")]
bitcoindir: Option<String>,
#[serde(default, deserialize_with = "default_on_error")]
blocksdir: Option<String>,
#[serde(default, deserialize_with = "default_on_error")]
rpcconnect: Option<String>,
#[serde(default, deserialize_with = "default_on_error")]
rpcport: Option<u16>,
#[serde(default, deserialize_with = "default_on_error")]
rpccookiefile: Option<String>,
#[serde(default, deserialize_with = "default_on_error")]
rpcuser: Option<String>,
#[serde(default, deserialize_with = "default_on_error")]
rpcpassword: Option<String>,
#[serde(default, deserialize_with = "default_on_error")]
check_collisions: Option<bool>,
}
impl Config {
pub fn import() -> Result<Self> {
let config_args = Self::parse_args();
let path = dot_brk_path();
let _ = fs::create_dir_all(&path);
let path = path.join("config.toml");
let mut config = Self::read(&path);
if let Some(v) = config_args.brkdir {
config.brkdir = Some(v);
}
if let Some(v) = config_args.brkport {
config.brkport = Some(v);
}
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.bitcoindir {
config.bitcoindir = Some(v);
}
if let Some(v) = config_args.blocksdir {
config.blocksdir = Some(v);
}
if let Some(v) = config_args.rpcconnect {
config.rpcconnect = Some(v);
}
if let Some(v) = config_args.rpcport {
config.rpcport = Some(v);
}
if let Some(v) = config_args.rpccookiefile {
config.rpccookiefile = Some(v);
}
if let Some(v) = config_args.rpcuser {
config.rpcuser = Some(v);
}
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)
}
fn parse_args() -> Self {
use lexopt::prelude::*;
let mut config = Self::default();
let mut parser = lexopt::Parser::from_env();
while let Some(arg) = parser.next().unwrap() {
match arg {
Short('h') | Long("help") => {
Self::print_help();
std::process::exit(0);
}
Short('V') | Long("version") => {
println!("brk {}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
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("rpcport") => config.rpcport = 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()),
_ => {
eprintln!("{}", arg.unexpected());
std::process::exit(1);
}
}
}
config
}
fn print_help() {
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")
);
}
fn check(&self) {
if !self.bitcoindir().is_dir() {
println!("{:?} isn't a valid directory", self.bitcoindir());
println!("Please use the --bitcoindir parameter to set a valid path.");
println!("Run the program with '-h' for help.");
std::process::exit(1);
}
if !self.blocksdir().is_dir() {
println!("{:?} isn't a valid directory", self.blocksdir());
println!("Please use the --blocksdir parameter to set a valid path.");
println!("Run the program with '-h' for help.");
std::process::exit(1);
}
if !self.brkdir().is_dir() {
println!("{:?} isn't a valid directory", self.brkdir());
println!("Please use the --brkdir parameter to set a valid path.");
println!("Run the program with '-h' for help.");
std::process::exit(1);
}
if self.rpc_auth().is_err() {
println!(
"Unsuccessful authentication with the RPC client.
First make sure that `bitcoind` is running. If it is then please either set --rpccookiefile or --rpcuser and --rpcpassword as the default values seemed to have failed.
Finally, you can run the program with '-h' for help."
);
std::process::exit(1);
}
}
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())
}
pub fn rpc(&self) -> Result<Client> {
Client::new(
&format!(
"http://{}:{}",
self.rpcconnect().unwrap_or(&"localhost".to_string()),
self.rpcport().unwrap_or(8332)
),
self.rpc_auth()?,
)
}
fn rpc_auth(&self) -> Result<Auth> {
let cookie = self.path_cookiefile();
if cookie.is_file() {
Ok(Auth::CookieFile(cookie))
} else if self.rpcuser.is_some() && self.rpcpassword.is_some() {
Ok(Auth::UserPass(
self.rpcuser.clone().unwrap(),
self.rpcpassword.clone().unwrap(),
))
} else {
Err(Error::AuthFailed)
}
}
fn rpcconnect(&self) -> Option<&String> {
self.rpcconnect.as_ref()
}
fn rpcport(&self) -> Option<u16> {
self.rpcport
}
pub fn bitcoindir(&self) -> PathBuf {
self.bitcoindir
.as_ref()
.map_or_else(Client::default_bitcoin_path, |s| fix_user_path(s.as_ref()))
}
pub fn blocksdir(&self) -> PathBuf {
self.blocksdir.as_ref().map_or_else(
|| self.bitcoindir().join("blocks"),
|blocksdir| fix_user_path(blocksdir.as_str()),
)
}
pub fn brkdir(&self) -> PathBuf {
self.brkdir
.as_ref()
.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"),
|p| fix_user_path(p.as_str()),
)
}
pub fn website(&self) -> WebsiteArg {
self.website.clone().unwrap_or_default()
}
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()),
}
}
+114
View File
@@ -0,0 +1,114 @@
#![doc = include_str!("../README.md")]
use std::{
fs,
thread::{self, sleep},
time::Duration,
};
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 tracing::info;
use vecdb::Exit;
mod config;
mod paths;
mod website;
use crate::{config::Config, paths::*, website::WebsiteArg};
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()))?;
let config = Config::import()?;
let client = config.rpc()?;
let exit = Exit::new();
exit.set_ctrlc_handler();
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())?;
let mempool = Mempool::new(&client);
let mempool_clone = mempool.clone();
thread::spawn(move || {
mempool_clone.start();
});
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 port = config.brkport();
let future = async move {
let server = Server::new(&query, data_path, website);
tokio::spawn(async move {
server.serve(port).await.unwrap();
});
Ok(()) as Result<()>
};
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
let _handle = runtime.spawn(future);
loop {
client.wait_for_synced_node()?;
let last_height = client.get_last_height()?;
info!("{} blocks found.", u32::from(last_height) + 1);
let starting_indexes = if config.check_collisions() {
indexer.checked_index(&blocks, &client, &exit)?
} else {
indexer.index(&blocks, &client, &exit)?
};
Mimalloc::collect();
computer.compute(&indexer, starting_indexes, &reader, &exit)?;
info!("Waiting for new blocks...");
while last_height == client.get_last_height()? {
sleep(Duration::from_secs(1))
}
}
}
+23
View File
@@ -0,0 +1,23 @@
use std::path::{Path, PathBuf};
pub fn dot_brk_path() -> PathBuf {
let home = std::env::var("HOME").unwrap();
Path::new(&home).join(".brk")
}
pub fn dot_brk_log_path() -> PathBuf {
dot_brk_path().join("log")
}
pub fn default_brk_path() -> PathBuf {
dot_brk_path()
}
pub fn fix_user_path(path: &str) -> PathBuf {
if let Some(rest) = path.strip_prefix("~/").or(path.strip_prefix("$HOME/"))
&& let Ok(home) = std::env::var("HOME")
{
return PathBuf::from(home).join(rest);
}
PathBuf::from(path)
}
+34
View File
@@ -0,0 +1,34 @@
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)),
})
}
}
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "brk_client"
description = "Rust client for the Bitcoin Research Kit API"
version.workspace = true
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
keywords = ["bitcoin", "blockchain", "analytics", "on-chain"]
categories = ["api-bindings", "cryptography::cryptocurrencies"]
[dependencies]
brk_cohort = { workspace = true }
brk_types = { workspace = true }
minreq = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
+53
View File
@@ -0,0 +1,53 @@
# brk_client
Rust client for the [Bitcoin Research Kit](https://github.com/bitcoinresearchkit/brk) API.
[crates.io](https://crates.io/crates/brk_client) | [docs.rs](https://docs.rs/brk_client)
## Installation
```toml
[dependencies]
brk_client = "0.1"
```
## Quick Start
```rust
use brk_client::{BrkClient, Index};
fn main() -> brk_client::Result<()> {
let client = BrkClient::new("http://localhost:3110");
// Blockchain data (mempool.space compatible)
let block = client.get_block_by_height(800000)?;
let tx = client.get_tx("abc123...")?;
let address = client.get_address("bc1q...")?;
// Metrics API - typed, chainable
let prices = client.metrics()
.price.usd.split.close
.by.dateindex()
.range(Some(-30), None)?; // Last 30 days
// Generic metric fetching
let data = client.get_metric(
"price_close".into(),
Index::DateIndex,
Some(-30), None, None, None,
)?;
Ok(())
}
```
## Configuration
```rust
use brk_client::{BrkClient, BrkClientOptions};
let client = BrkClient::with_options(BrkClientOptions {
base_url: "http://localhost:3110".to_string(),
timeout_secs: 60,
});
```
+82
View File
@@ -0,0 +1,82 @@
//! Basic example of using the BRK client.
use brk_client::{BrkClient, BrkClientOptions};
use brk_types::{FormatResponse, Index, Metric};
fn main() -> brk_client::Result<()> {
// Create client with default options
let client = BrkClient::new("http://localhost:3110");
// Or with custom options
let _client_with_options = BrkClient::with_options(BrkClientOptions {
base_url: "http://localhost:3110".to_string(),
timeout_secs: 60,
});
// Fetch price data using the typed metrics API
// Using new idiomatic API: last(3).fetch()
let price_close = client
.metrics()
.price
.usd
.split
.close
.by
.dateindex()
.last(3)
.fetch()?;
println!("Last 3 price close values: {:?}", price_close);
// Fetch block data
let block_count = client
.metrics()
.blocks
.count
.block_count
.sum
.by
.dateindex()
.last(3)
.fetch()?;
println!("Last 3 block count values: {:?}", block_count);
// Fetch supply data
dbg!(
client
.metrics()
.supply
.circulating
.bitcoin
.by
.dateindex()
.path()
);
let circulating = client
.metrics()
.supply
.circulating
.bitcoin
.by
.dateindex()
.last(3)
.fetch_csv()?;
println!("Last 3 circulating supply values: {:?}", circulating);
// Using generic metric fetching
let metricdata = client.get_metric(
Metric::from("price_close"),
Index::DateIndex,
Some(-3),
None,
None,
None,
)?;
match metricdata {
FormatResponse::Json(m) => {
println!("Generic fetch result count: {}", m.data.len());
}
FormatResponse::Csv(_) => panic!(),
};
Ok(())
}
+85
View File
@@ -0,0 +1,85 @@
//! Comprehensive test that fetches all endpoints in the tree.
//!
//! This example demonstrates how to recursively traverse the metrics catalog tree
//! and fetch data from each endpoint. Run with: cargo run --example tree
use brk_client::BrkClient;
use brk_types::{Index, TreeNode};
use std::collections::BTreeSet;
/// A collected metric with its path and available indexes.
struct CollectedMetric {
path: String,
name: String,
indexes: BTreeSet<Index>,
}
/// Recursively collect all metrics from the tree.
fn collect_metrics(node: &TreeNode, path: &str) -> Vec<CollectedMetric> {
let mut metrics = Vec::new();
match node {
TreeNode::Branch(children) => {
for (key, child) in children {
let child_path = if path.is_empty() {
key.clone()
} else {
format!("{}.{}", path, key)
};
metrics.extend(collect_metrics(child, &child_path));
}
}
TreeNode::Leaf(leaf) => {
metrics.push(CollectedMetric {
path: path.to_string(),
name: leaf.name().to_string(),
indexes: leaf.indexes().clone(),
});
}
}
metrics
}
fn main() -> brk_client::Result<()> {
let client = BrkClient::new("http://localhost:3110");
// Get the metrics catalog tree
let tree = client.get_metrics_tree()?;
// Recursively collect all metrics
let metrics = collect_metrics(&tree, "");
println!("\nFound {} metrics", metrics.len());
let mut success = 0;
for metric in &metrics {
for index in &metric.indexes {
let index_str = index.serialize_long();
let full_path = format!("{}.by.{}", metric.path, index_str);
match client.get_metric(
metric.name.as_str().into(),
*index,
None,
Some(0),
None,
None,
) {
Ok(_) => {
success += 1;
println!("OK: {}", full_path);
}
Err(e) => {
println!("FAIL: {} -> {}", full_path, e);
return Err(e);
}
}
}
}
println!("\n=== Results ===");
println!("Success: {}", success);
Ok(())
}

Some files were not shown because too many files have changed in this diff Show More