mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-09 22:43:33 -07:00
global: snapshot
This commit is contained in:
Generated
+19
-11
@@ -720,8 +720,7 @@ dependencies = [
|
||||
"brk_structs",
|
||||
"brk_traversable",
|
||||
"derive_deref",
|
||||
"quick_cache",
|
||||
"rustc-hash",
|
||||
"quickmatch",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2864,11 +2863,11 @@ checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.1"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3761,6 +3760,15 @@ dependencies = [
|
||||
"parking_lot 0.12.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quickmatch"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2da298bd4f885924ca16faab70af76893797eb0164dfa4e86a14ffd983b8cb14"
|
||||
dependencies = [
|
||||
"rustc-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.41"
|
||||
@@ -3943,9 +3951,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.3"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c"
|
||||
checksum = "4a52d8d02cacdb176ef4678de6c052efb4b3da14b78e4db683a4252762be5433"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -3955,9 +3963,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.11"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad"
|
||||
checksum = "722166aa0d7438abbaa4d5cc2c649dac844e8c56d82fb3d33e9c34b5cd268fc6"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -3966,9 +3974,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.6"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
|
||||
checksum = "c3160422bbd54dd5ecfdca71e5fd59b7b8fe2b1697ab2baf64f6d05dcc66d298"
|
||||
|
||||
[[package]]
|
||||
name = "regress"
|
||||
|
||||
@@ -70,7 +70,6 @@ jiff = "0.2.15"
|
||||
log = "0.4.28"
|
||||
minreq = { version = "2.14.1", features = ["https", "serde_json"] }
|
||||
parking_lot = "0.12.5"
|
||||
quick_cache = "0.6.17"
|
||||
rayon = "1.11.0"
|
||||
schemars = "1.0.4"
|
||||
serde = "1.0.228"
|
||||
|
||||
@@ -18,10 +18,10 @@ brk_indexer = { workspace = true }
|
||||
brk_parser = { workspace = true }
|
||||
brk_structs = { workspace = true }
|
||||
brk_traversable = { workspace = true }
|
||||
vecdb = { workspace = true }
|
||||
derive_deref = { workspace = true }
|
||||
quick_cache = { workspace = true }
|
||||
# quickmatch = { path = "../../../quickmatch" }
|
||||
quickmatch = "0.1.8"
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
rustc-hash = "2.1.1"
|
||||
vecdb = { workspace = true }
|
||||
|
||||
@@ -7,8 +7,8 @@ use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_parser::Parser;
|
||||
use brk_structs::{
|
||||
AddressInfo, AddressPath, Format, Height, Index, IndexInfo, MetricCount, TransactionInfo,
|
||||
TxidPath,
|
||||
AddressInfo, AddressPath, Format, Height, Index, IndexInfo, MetricCount, MetricSearchQuery,
|
||||
TransactionInfo, TxidPath,
|
||||
};
|
||||
use brk_traversable::TreeNode;
|
||||
use vecdb::{AnyCollectableVec, AnyStoredVec};
|
||||
@@ -18,7 +18,6 @@ mod deser;
|
||||
mod metrics;
|
||||
mod pagination;
|
||||
mod params;
|
||||
mod searcher;
|
||||
mod vecs;
|
||||
|
||||
pub use metrics::{Output, Value};
|
||||
@@ -71,8 +70,8 @@ impl<'a> Interface<'a> {
|
||||
get_transaction_info(txid, self)
|
||||
}
|
||||
|
||||
pub fn search_metric(&self, metric: &str, limit: usize) -> Vec<&str> {
|
||||
self.vecs.search(metric, limit)
|
||||
pub fn match_metric(&self, query: MetricSearchQuery) -> Vec<&str> {
|
||||
self.vecs.matches(query)
|
||||
}
|
||||
|
||||
pub fn search_metric_with_index(
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
use std::{marker::PhantomData, ops::Neg, ptr};
|
||||
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
const MAX_TRIGRAMS: usize = 9;
|
||||
|
||||
pub struct NgramSearcher<'a> {
|
||||
max_word_count: usize,
|
||||
max_word_len: usize,
|
||||
max_query_len: usize,
|
||||
word_index: FxHashMap<String, FxHashSet<*const str>>,
|
||||
trigram_index: FxHashMap<[char; 3], FxHashSet<*const str>>,
|
||||
_phantom: PhantomData<&'a str>,
|
||||
}
|
||||
|
||||
unsafe impl<'a> Send for NgramSearcher<'a> {}
|
||||
unsafe impl<'a> Sync for NgramSearcher<'a> {}
|
||||
|
||||
const SEPARATORS: &[char] = &['_', '-', ' '];
|
||||
|
||||
impl<'a> NgramSearcher<'a> {
|
||||
pub fn new(items: &[&'a str]) -> Self {
|
||||
let mut word_index: FxHashMap<String, FxHashSet<*const str>> = FxHashMap::default();
|
||||
let mut trigram_index: FxHashMap<[char; 3], FxHashSet<*const str>> = FxHashMap::default();
|
||||
let mut max_word_len = 0;
|
||||
let mut max_query_len = 0;
|
||||
let mut max_words = 0;
|
||||
|
||||
for &item in items {
|
||||
max_query_len = max_query_len.max(item.len());
|
||||
let mut word_count = 0;
|
||||
for word in item.split(SEPARATORS) {
|
||||
word_count += 1;
|
||||
if word.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
max_word_len = max_word_len.max(item.len());
|
||||
|
||||
word_index.entry(word.to_string()).or_default().insert(item);
|
||||
|
||||
if word.len() >= 3 {
|
||||
let chars = word.chars().collect::<Vec<_>>();
|
||||
for window in chars.windows(3) {
|
||||
trigram_index
|
||||
.entry(unsafe { ptr::read(window.as_ptr() as *const [char; 3]) })
|
||||
.or_default()
|
||||
.insert(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
max_words = max_words.max(word_count);
|
||||
}
|
||||
|
||||
Self {
|
||||
max_query_len: max_query_len + 6,
|
||||
max_word_len: max_word_len + 4,
|
||||
max_word_count: max_word_len + 2,
|
||||
word_index,
|
||||
trigram_index,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn search(&self, query: &str, limit: usize) -> Vec<&'a str> {
|
||||
let query_lower = query.to_lowercase();
|
||||
let query_len = query_lower.len();
|
||||
|
||||
if query.is_empty() || query_len > self.max_query_len {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let words: FxHashSet<&str> = query_lower
|
||||
.split(SEPARATORS)
|
||||
.filter(|w| !w.is_empty() && w.len() <= self.max_word_len)
|
||||
.collect();
|
||||
|
||||
if words.is_empty() || words.len() > self.max_word_count {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let min_len = query_len.saturating_sub(3);
|
||||
|
||||
let mut pool: Option<FxHashSet<*const str>> = None;
|
||||
let mut unknown_words = Vec::new();
|
||||
|
||||
let mut words_to_intersect = vec![];
|
||||
for word in words {
|
||||
match self.word_index.get(word) {
|
||||
Some(items) => words_to_intersect.push(items),
|
||||
None => unknown_words.push(word),
|
||||
}
|
||||
}
|
||||
|
||||
if !words_to_intersect.is_empty() {
|
||||
words_to_intersect.sort_unstable_by_key(|set| (set.len() as i64).neg());
|
||||
|
||||
let mut intersect = words_to_intersect.pop().cloned().unwrap();
|
||||
|
||||
for other_set in words_to_intersect.iter().rev() {
|
||||
intersect.retain(|ptr| other_set.contains(ptr));
|
||||
if intersect.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
pool = Some(intersect);
|
||||
}
|
||||
let some_pool = pool.is_some();
|
||||
|
||||
if some_pool && unknown_words.is_empty() {
|
||||
let mut results: Vec<_> = pool
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|item| unsafe { &*item as &str })
|
||||
.collect();
|
||||
// Partial sort - only sort what we need
|
||||
if results.len() > limit {
|
||||
results.select_nth_unstable_by_key(limit, |item| item.len());
|
||||
results.truncate(limit);
|
||||
}
|
||||
results.sort_unstable_by_key(|item| item.len());
|
||||
return results;
|
||||
}
|
||||
|
||||
// Score candidates
|
||||
let mut scores: FxHashMap<*const str, usize> = FxHashMap::default();
|
||||
scores.reserve(256);
|
||||
if let Some(pool) = &pool {
|
||||
for &item in pool {
|
||||
scores.insert(item, 1);
|
||||
}
|
||||
}
|
||||
let mut trigram_count = 0;
|
||||
'outer: for word in unknown_words {
|
||||
if word.len() < 3 || trigram_count >= MAX_TRIGRAMS {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut chars = word.chars();
|
||||
let mut a = chars.next().unwrap();
|
||||
let mut b = chars.next().unwrap();
|
||||
|
||||
for c in chars {
|
||||
if trigram_count >= MAX_TRIGRAMS {
|
||||
break 'outer;
|
||||
}
|
||||
trigram_count += 1;
|
||||
|
||||
let trigram = [a, b, c];
|
||||
|
||||
let Some(items) = self.trigram_index.get(&trigram) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if some_pool {
|
||||
for &item in items {
|
||||
if let Some(score) = scores.get_mut(&item) {
|
||||
*score += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for &item in items {
|
||||
let len = unsafe { &*item }.len();
|
||||
if len >= min_len {
|
||||
*scores.entry(item).or_default() += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Slide window
|
||||
a = b;
|
||||
b = c;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by minimum score
|
||||
let min_score = trigram_count.div_ceil(2);
|
||||
let mut results: Vec<_> = scores
|
||||
.into_iter()
|
||||
.filter(|(_, s)| *s >= min_score)
|
||||
.map(|(item, score)| (unsafe { &*item as &str }, score))
|
||||
.collect();
|
||||
|
||||
if results.len() > limit {
|
||||
results.select_nth_unstable_by(limit, |a, b| {
|
||||
b.1.cmp(&a.1).then_with(|| a.0.len().cmp(&b.0.len()))
|
||||
});
|
||||
results.truncate(limit);
|
||||
}
|
||||
|
||||
results.sort_unstable_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.len().cmp(&b.0.len())));
|
||||
|
||||
results
|
||||
.into_iter()
|
||||
.take(limit)
|
||||
.map(|(item, _)| item)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,13 @@ use std::collections::BTreeMap;
|
||||
|
||||
use brk_computer::Computer;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_structs::{Index, IndexInfo};
|
||||
use brk_structs::{Index, IndexInfo, MetricSearchQuery};
|
||||
use brk_traversable::{Traversable, TreeNode};
|
||||
use derive_deref::{Deref, DerefMut};
|
||||
use quickmatch::{QuickMatch, QuickMatchConfig};
|
||||
use vecdb::AnyCollectableVec;
|
||||
|
||||
use crate::{
|
||||
pagination::{PaginatedIndexParam, PaginatedMetrics, PaginationParam},
|
||||
searcher::NgramSearcher,
|
||||
};
|
||||
use crate::pagination::{PaginatedIndexParam, PaginatedMetrics, PaginationParam};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Vecs<'a> {
|
||||
@@ -22,7 +20,7 @@ pub struct Vecs<'a> {
|
||||
pub total_metric_count: usize,
|
||||
pub longest_metric_len: usize,
|
||||
catalog: Option<TreeNode>,
|
||||
searcher: Option<NgramSearcher<'a>>,
|
||||
matcher: Option<QuickMatch<'a>>,
|
||||
metric_to_indexes: BTreeMap<&'a str, Vec<Index>>,
|
||||
index_to_metrics: BTreeMap<Index, Vec<&'a str>>,
|
||||
}
|
||||
@@ -106,7 +104,7 @@ impl<'a> Vecs<'a> {
|
||||
.simplify()
|
||||
.unwrap(),
|
||||
);
|
||||
this.searcher = Some(NgramSearcher::new(&this.metrics));
|
||||
this.matcher = Some(QuickMatch::new(&this.metrics));
|
||||
|
||||
this
|
||||
}
|
||||
@@ -174,12 +172,13 @@ impl<'a> Vecs<'a> {
|
||||
self.catalog.as_ref().unwrap()
|
||||
}
|
||||
|
||||
pub fn search(&self, metric: &str, limit: usize) -> Vec<&'_ str> {
|
||||
self.searcher().search(metric, limit)
|
||||
pub fn matches(&self, query: MetricSearchQuery) -> Vec<&'_ str> {
|
||||
self.matcher()
|
||||
.matches_with(&query.q, &QuickMatchConfig::new().with_limit(query.limit))
|
||||
}
|
||||
|
||||
fn searcher(&self) -> &NgramSearcher<'_> {
|
||||
self.searcher.as_ref().unwrap()
|
||||
fn matcher(&self) -> &QuickMatch<'_> {
|
||||
self.matcher.as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ brk_traversable = { workspace = true }
|
||||
vecdb = { workspace = true }
|
||||
jiff = { workspace = true }
|
||||
log = { workspace = true }
|
||||
quick_cache = { workspace = true }
|
||||
quick_cache = "0.6.17"
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
sonic-rs = { workspace = true }
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use aide::axum::{ApiRouter, routing::get_with};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
http::HeaderMap,
|
||||
response::Response,
|
||||
};
|
||||
use brk_structs::{AddressInfo, AddressPath};
|
||||
|
||||
use crate::extended::{ResponseExtended, ResultExtended, TransformResponseExtended};
|
||||
use crate::{
|
||||
VERSION,
|
||||
extended::{HeaderMapExtended, ResponseExtended, ResultExtended, TransformResponseExtended},
|
||||
};
|
||||
|
||||
use super::AppState;
|
||||
|
||||
@@ -19,14 +21,19 @@ impl AddressesRoutes for ApiRouter<AppState> {
|
||||
fn add_addresses_routes(self) -> Self {
|
||||
self.api_route(
|
||||
"/api/chain/address/{address}",
|
||||
get_with(async |Path(address): Path<AddressPath>,
|
||||
State(app_state): State<AppState>|
|
||||
-> Result<Response, (StatusCode, Json<String>)> {
|
||||
let address_info = app_state.interface.get_address_info(address).to_server_result()?;
|
||||
|
||||
let bytes = sonic_rs::to_vec(&address_info).unwrap();
|
||||
|
||||
Ok(Response::new_json_from_bytes(bytes))
|
||||
get_with(async |
|
||||
headers: HeaderMap,
|
||||
Path(address): Path<AddressPath>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
let etag = format!("{VERSION}-{}", state.get_height());
|
||||
if headers.has_etag(&etag) {
|
||||
return Response::new_not_modified();
|
||||
}
|
||||
match state.get_address_info(address).with_status() {
|
||||
Ok(value) => Response::new_json(&value, &etag),
|
||||
Err((status, message)) => Response::new_json_with(status, &message, &etag)
|
||||
}
|
||||
}, |op| op
|
||||
.tag("Chain")
|
||||
.summary("Address information")
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use aide::axum::{ApiRouter, routing::get_with};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
http::HeaderMap,
|
||||
response::Response,
|
||||
};
|
||||
use brk_structs::{TransactionInfo, TxidPath};
|
||||
|
||||
use crate::extended::{ResponseExtended, ResultExtended, TransformResponseExtended};
|
||||
use crate::{
|
||||
VERSION,
|
||||
extended::{HeaderMapExtended, ResponseExtended, ResultExtended, TransformResponseExtended},
|
||||
};
|
||||
|
||||
use super::AppState;
|
||||
|
||||
@@ -20,14 +22,19 @@ impl TransactionsRoutes for ApiRouter<AppState> {
|
||||
self.api_route(
|
||||
"/api/chain/tx/{txid}",
|
||||
get_with(
|
||||
async |Path(txid): Path<TxidPath>,
|
||||
State(app_state): State<AppState>|
|
||||
-> Result<Response, (StatusCode, Json<String>)> {
|
||||
let tx_info = app_state.interface.get_transaction_info(txid).to_server_result()?;
|
||||
|
||||
let bytes = sonic_rs::to_vec(&tx_info).unwrap();
|
||||
|
||||
Ok(Response::new_json_from_bytes(bytes))
|
||||
async |
|
||||
headers: HeaderMap,
|
||||
Path(txid): Path<TxidPath>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
let etag = format!("{VERSION}-{}", state.get_height());
|
||||
if headers.has_etag(&etag) {
|
||||
return Response::new_not_modified();
|
||||
}
|
||||
match state.get_transaction_info(txid).with_status() {
|
||||
Ok(value) => Response::new_json(&value, &etag),
|
||||
Err((status, message)) => Response::new_json_with(status, &message, &etag)
|
||||
}
|
||||
},
|
||||
|op| op
|
||||
.tag("Chain")
|
||||
|
||||
@@ -23,9 +23,9 @@ pub async fn handler(
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
query: Query<Params>,
|
||||
State(app_state): State<AppState>,
|
||||
State(state): State<AppState>,
|
||||
) -> Response {
|
||||
match req_to_response_res(uri, headers, query, app_state) {
|
||||
match req_to_response_res(uri, headers, query, state) {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
let mut response =
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use aide::axum::{ApiRouter, routing::get_with};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, Query, State},
|
||||
http::{HeaderMap, StatusCode, Uri},
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
routing::get,
|
||||
};
|
||||
use brk_interface::{PaginatedMetrics, PaginationParam, Params, ParamsDeprec, ParamsOpt};
|
||||
use brk_structs::{Index, IndexInfo, MetricCount, MetricPath};
|
||||
use brk_structs::{Index, IndexInfo, MetricCount, MetricPath, MetricSearchQuery};
|
||||
use brk_traversable::TreeNode;
|
||||
|
||||
use crate::{
|
||||
@@ -23,8 +22,6 @@ pub trait ApiMetricsRoutes {
|
||||
fn add_metrics_routes(self) -> Self;
|
||||
}
|
||||
|
||||
const TO_SEPARATOR: &str = "_to_";
|
||||
|
||||
impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
fn add_metrics_routes(self) -> Self {
|
||||
self
|
||||
@@ -32,149 +29,145 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
.api_route(
|
||||
"/api/metrics/count",
|
||||
get_with(
|
||||
async |State(app_state): State<AppState>| {
|
||||
Json(app_state.interface.metric_count())
|
||||
},
|
||||
|op| {
|
||||
op.tag("Metrics")
|
||||
.summary("Metric count")
|
||||
.description("Current metric count")
|
||||
.with_ok_response::<Vec<MetricCount>, _>(|res| res)
|
||||
.with_not_modified()
|
||||
async |
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
let etag = VERSION;
|
||||
if headers.has_etag(etag) {
|
||||
return Response::new_not_modified();
|
||||
}
|
||||
Response::new_json(state.metric_count(), etag)
|
||||
},
|
||||
|op| op.tag("Metrics")
|
||||
.summary("Metric count")
|
||||
.description("Current metric count")
|
||||
.with_ok_response::<Vec<MetricCount>, _>(|res| res)
|
||||
.with_not_modified(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/metrics/indexes",
|
||||
get_with(
|
||||
async |State(app_state): State<AppState>| {
|
||||
Json(app_state.interface.get_indexes())
|
||||
},
|
||||
|op| {
|
||||
op.tag("Metrics")
|
||||
.summary("List available indexes")
|
||||
.description(
|
||||
"Returns all available indexes with their accepted query aliases. Use any alias when querying metrics."
|
||||
)
|
||||
.with_ok_response::<Vec<IndexInfo>, _>(|res| res)
|
||||
.with_not_modified()
|
||||
async |
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
let etag = VERSION;
|
||||
if headers.has_etag(etag) {
|
||||
return Response::new_not_modified();
|
||||
}
|
||||
Response::new_json(state.get_indexes(), etag)
|
||||
},
|
||||
|op| op.tag("Metrics")
|
||||
.summary("List available indexes")
|
||||
.description(
|
||||
"Returns all available indexes with their accepted query aliases. Use any alias when querying metrics."
|
||||
)
|
||||
.with_ok_response::<Vec<IndexInfo>, _>(|res| res)
|
||||
.with_not_modified(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/metrics/list",
|
||||
get_with(
|
||||
async |State(app_state): State<AppState>,
|
||||
Query(pagination): Query<PaginationParam>| {
|
||||
Json(app_state.interface.get_metrics(pagination))
|
||||
},
|
||||
|op| {
|
||||
op.tag("Metrics")
|
||||
.summary("Metrics list")
|
||||
.description("Paginated list of available metrics")
|
||||
.with_ok_response::<PaginatedMetrics, _>(|res| res)
|
||||
.with_not_modified()
|
||||
async |
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>,
|
||||
Query(pagination): Query<PaginationParam>
|
||||
| {
|
||||
let etag = VERSION;
|
||||
if headers.has_etag(etag) {
|
||||
return Response::new_not_modified();
|
||||
}
|
||||
Response::new_json(state.get_metrics(pagination), etag)
|
||||
},
|
||||
|op| op.tag("Metrics")
|
||||
.summary("Metrics list")
|
||||
.description("Paginated list of available metrics")
|
||||
.with_ok_response::<PaginatedMetrics, _>(|res| res)
|
||||
.with_not_modified(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/metrics/catalog",
|
||||
get_with(
|
||||
async |headers: HeaderMap, State(app_state): State<AppState>| -> Response {
|
||||
async |headers: HeaderMap, State(state): State<AppState>| -> Response {
|
||||
let etag = VERSION;
|
||||
|
||||
if headers
|
||||
.get_if_none_match()
|
||||
.is_some_and(|prev_etag| etag == prev_etag)
|
||||
{
|
||||
if headers.has_etag(etag) {
|
||||
return Response::new_not_modified();
|
||||
}
|
||||
|
||||
let bytes = sonic_rs::to_vec(&app_state.interface.get_metrics_catalog()).unwrap();
|
||||
|
||||
let mut response = Response::new_json_from_bytes(bytes);
|
||||
|
||||
let headers = response.headers_mut();
|
||||
headers.insert_cors();
|
||||
headers.insert_etag(etag);
|
||||
|
||||
response
|
||||
Response::new_json(state.get_metrics_catalog(), etag)
|
||||
},
|
||||
|op| {
|
||||
op.tag("Metrics")
|
||||
|op| op.tag("Metrics")
|
||||
.summary("Metrics catalog")
|
||||
.description(
|
||||
"Returns the complete hierarchical catalog of available metrics organized as a tree structure. Metrics are grouped by categories and subcategories. Best viewed in an interactive JSON viewer (e.g., Firefox's built-in JSON viewer) for easy navigation of the nested structure."
|
||||
)
|
||||
.with_ok_response::<TreeNode, _>(|res| res)
|
||||
.with_not_modified()
|
||||
},
|
||||
.with_not_modified(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/search/{metric}",
|
||||
"/api/metrics/search",
|
||||
get_with(
|
||||
async |
|
||||
headers: HeaderMap,
|
||||
State(app_state): State<AppState>,
|
||||
Path(MetricPath { metric }): Path<MetricPath>
|
||||
State(state): State<AppState>,
|
||||
Query(query): Query<MetricSearchQuery>
|
||||
| {
|
||||
let etag = VERSION;
|
||||
|
||||
if headers
|
||||
.get_if_none_match()
|
||||
.is_some_and(|prev_etag| etag == prev_etag)
|
||||
{
|
||||
if headers.has_etag(etag) {
|
||||
return Response::new_not_modified();
|
||||
}
|
||||
|
||||
let bytes = sonic_rs::to_vec(&app_state.interface.search_metric(&metric, usize::MAX)).unwrap();
|
||||
|
||||
let mut response = Response::new_json_from_bytes(bytes);
|
||||
|
||||
let headers = response.headers_mut();
|
||||
headers.insert_cors();
|
||||
headers.insert_etag(etag);
|
||||
|
||||
response
|
||||
},
|
||||
|op| {
|
||||
op.tag("Metrics")
|
||||
.summary("Metric search")
|
||||
.description(
|
||||
"Search metrics based on a query"
|
||||
)
|
||||
.with_ok_response::<Vec<String>, _>(|res| res)
|
||||
.with_not_modified()
|
||||
Response::new_json(state.match_metric(query), etag)
|
||||
},
|
||||
|op| op.tag("Metrics")
|
||||
.summary("Search metrics")
|
||||
.description("Fuzzy search for metrics by name. Supports partial matches and typos.")
|
||||
.with_ok_response::<Vec<String>, _>(|res| res)
|
||||
.with_not_modified(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/metrics/{metric}",
|
||||
get_with(
|
||||
async |
|
||||
State(app_state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>,
|
||||
Path(MetricPath { metric }): Path<MetricPath>
|
||||
| {
|
||||
match app_state.interface.metric_to_indexes(metric) {
|
||||
Some(indexes) => Json(indexes).into_response(),
|
||||
None => StatusCode::NOT_FOUND.into_response()
|
||||
let etag = VERSION;
|
||||
if headers.has_etag(etag) {
|
||||
return Response::new_not_modified();
|
||||
}
|
||||
if let Some(indexes) = state.metric_to_indexes(metric.clone()) {
|
||||
return Response::new_json(indexes, etag)
|
||||
}
|
||||
let value = if let Some(first) = state.match_metric(MetricSearchQuery {
|
||||
q: metric.clone(),
|
||||
limit: 1,
|
||||
}).first() {
|
||||
format!("Could not find '{metric}', did you mean '{first}' ?")
|
||||
} else {
|
||||
format!("Could not find '{metric}'.")
|
||||
};
|
||||
Response::new_json_with(StatusCode::NOT_FOUND, value, etag)
|
||||
},
|
||||
|op| {
|
||||
op.tag("Metrics")
|
||||
.summary("Get supported indexes for a metric")
|
||||
.description(
|
||||
"Returns the list of indexes are supported by the specified metric. \
|
||||
For example, `realized_price` might be available on dateindex, weekindex, and monthindex."
|
||||
)
|
||||
.with_ok_response::<Vec<Index>, _>(|res| res)
|
||||
.with_not_modified()
|
||||
.with_not_found()
|
||||
},
|
||||
|op| op.tag("Metrics")
|
||||
.summary("Get supported indexes for a metric")
|
||||
.description(
|
||||
"Returns the list of indexes are supported by the specified metric. \
|
||||
For example, `realized_price` might be available on dateindex, weekindex, and monthindex."
|
||||
)
|
||||
.with_ok_response::<Vec<Index>, _>(|res| res)
|
||||
.with_not_modified()
|
||||
.with_not_found(),
|
||||
),
|
||||
)
|
||||
// WIP
|
||||
.route("/api/metrics/bulk", get(data::handler))
|
||||
// WIP
|
||||
.route(
|
||||
"/api/metrics/{metric}/{index}",
|
||||
get(
|
||||
@@ -221,8 +214,9 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
Query(params_opt): Query<ParamsOpt>,
|
||||
state: State<AppState>|
|
||||
-> Response {
|
||||
let separator = "_to_";
|
||||
let variant = variant.replace("-", "_");
|
||||
let mut split = variant.split(TO_SEPARATOR);
|
||||
let mut split = variant.split(separator);
|
||||
|
||||
let ser_index = split.next().unwrap();
|
||||
let Ok(index) = Index::try_from(ser_index) else {
|
||||
@@ -230,7 +224,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
};
|
||||
|
||||
let params = Params::from((
|
||||
(index, split.collect::<Vec<_>>().join(TO_SEPARATOR)),
|
||||
(index, split.collect::<Vec<_>>().join(separator)),
|
||||
params_opt,
|
||||
));
|
||||
data::handler(uri, headers, Query(params), state).await
|
||||
|
||||
@@ -52,8 +52,8 @@ impl ApiRoutes for ApiRouter<AppState> {
|
||||
get_with(
|
||||
async || -> Json<Health> {
|
||||
Json(Health {
|
||||
status: "healthy".to_string(),
|
||||
service: "brk".to_string(),
|
||||
status: "healthy",
|
||||
service: "brk",
|
||||
timestamp: jiff::Timestamp::now().to_string(),
|
||||
})
|
||||
},
|
||||
@@ -73,21 +73,11 @@ impl ApiRoutes for ApiRouter<AppState> {
|
||||
-> Response {
|
||||
let etag = VERSION;
|
||||
|
||||
if headers
|
||||
.get_if_none_match()
|
||||
.is_some_and(|prev_etag| etag == prev_etag)
|
||||
{
|
||||
if headers.has_etag(etag) {
|
||||
return Response::new_not_modified();
|
||||
}
|
||||
|
||||
let mut response =
|
||||
Response::new_json_from_bytes(sonic_rs::to_vec(&api).unwrap());
|
||||
|
||||
let headers = response.headers_mut();
|
||||
headers.insert_cors();
|
||||
headers.insert_etag(etag);
|
||||
|
||||
response
|
||||
Response::new_json(&api, etag)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -21,7 +21,7 @@ pub enum ModifiedState {
|
||||
pub trait HeaderMapExtended {
|
||||
fn insert_cors(&mut self);
|
||||
|
||||
fn get_if_none_match(&self) -> Option<&str>;
|
||||
fn has_etag(&self, etag: &str) -> bool;
|
||||
|
||||
fn get_if_modified_since(&self) -> Option<DateTime>;
|
||||
fn check_if_modified_since(&self, path: &Path) -> Result<(ModifiedState, DateTime)>;
|
||||
@@ -123,8 +123,9 @@ impl HeaderMapExtended for HeaderMap {
|
||||
None
|
||||
}
|
||||
|
||||
fn get_if_none_match(&self) -> Option<&str> {
|
||||
self.get(IF_NONE_MATCH).and_then(|v| v.to_str().ok())
|
||||
fn has_etag(&self, etag: &str) -> bool {
|
||||
self.get(IF_NONE_MATCH)
|
||||
.is_some_and(|prev_etag| etag == prev_etag)
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
||||
|
||||
@@ -11,7 +11,12 @@ where
|
||||
Self: Sized,
|
||||
{
|
||||
fn new_not_modified() -> Self;
|
||||
fn new_json_from_bytes(bytes: Vec<u8>) -> Self;
|
||||
fn new_json<T>(value: T, etag: &str) -> Self
|
||||
where
|
||||
T: sonic_rs::Serialize;
|
||||
fn new_json_with<T>(status: StatusCode, value: T, etag: &str) -> Self
|
||||
where
|
||||
T: sonic_rs::Serialize;
|
||||
}
|
||||
|
||||
impl ResponseExtended for Response<Body> {
|
||||
@@ -22,10 +27,25 @@ impl ResponseExtended for Response<Body> {
|
||||
response
|
||||
}
|
||||
|
||||
fn new_json_from_bytes(bytes: Vec<u8>) -> Self {
|
||||
Response::builder()
|
||||
.header("content-type", "application/json")
|
||||
.body(bytes.into())
|
||||
.unwrap()
|
||||
fn new_json<T>(value: T, etag: &str) -> Self
|
||||
where
|
||||
T: sonic_rs::Serialize,
|
||||
{
|
||||
Self::new_json_with(StatusCode::default(), value, etag)
|
||||
}
|
||||
|
||||
fn new_json_with<T>(status: StatusCode, value: T, etag: &str) -> Self
|
||||
where
|
||||
T: sonic_rs::Serialize,
|
||||
{
|
||||
let bytes = sonic_rs::to_vec(&value).unwrap();
|
||||
let mut response = Response::builder().body(bytes.into()).unwrap();
|
||||
*response.status_mut() = status;
|
||||
let headers = response.headers_mut();
|
||||
headers.insert_cors();
|
||||
headers.insert_content_type_application_json();
|
||||
headers.insert_cache_control_must_revalidate();
|
||||
headers.insert_etag(etag);
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use axum::{Json, http::StatusCode};
|
||||
use axum::http::StatusCode;
|
||||
use brk_error::{Error, Result};
|
||||
|
||||
pub trait ResultExtended<T> {
|
||||
fn to_server_result(self) -> Result<T, (StatusCode, Json<String>)>;
|
||||
fn with_status(self) -> Result<T, (StatusCode, String)>;
|
||||
}
|
||||
|
||||
impl<T> ResultExtended<T> for Result<T> {
|
||||
fn to_server_result(self) -> Result<T, (StatusCode, Json<String>)> {
|
||||
fn with_status(self) -> Result<T, (StatusCode, String)> {
|
||||
self.map_err(|e| {
|
||||
(
|
||||
match e {
|
||||
@@ -17,7 +17,7 @@ impl<T> ResultExtended<T> for Result<T> {
|
||||
Error::UnknownAddress | Error::UnknownTxid => StatusCode::NOT_FOUND,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
},
|
||||
Json(e.to_string()),
|
||||
e.to_string(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,22 +14,22 @@ use crate::{AppState, HeaderMapExtended, ModifiedState, ResponseExtended};
|
||||
|
||||
pub async fn file_handler(
|
||||
headers: HeaderMap,
|
||||
State(app_state): State<AppState>,
|
||||
State(state): State<AppState>,
|
||||
path: extract::Path<String>,
|
||||
) -> Response {
|
||||
any_handler(headers, app_state, Some(path))
|
||||
any_handler(headers, state, Some(path))
|
||||
}
|
||||
|
||||
pub async fn index_handler(headers: HeaderMap, State(app_state): State<AppState>) -> Response {
|
||||
any_handler(headers, app_state, None)
|
||||
pub async fn index_handler(headers: HeaderMap, State(state): State<AppState>) -> Response {
|
||||
any_handler(headers, state, None)
|
||||
}
|
||||
|
||||
fn any_handler(
|
||||
headers: HeaderMap,
|
||||
app_state: AppState,
|
||||
state: AppState,
|
||||
path: Option<extract::Path<String>>,
|
||||
) -> Response {
|
||||
let files_path = app_state.path.as_ref().unwrap();
|
||||
let files_path = state.path.as_ref().unwrap();
|
||||
|
||||
if let Some(path) = path.as_ref() {
|
||||
let path = path.0.replace("..", "").replace("\\", "");
|
||||
@@ -52,14 +52,14 @@ fn any_handler(
|
||||
}
|
||||
}
|
||||
|
||||
path_to_response(&headers, &app_state, &path)
|
||||
path_to_response(&headers, &state, &path)
|
||||
} else {
|
||||
path_to_response(&headers, &app_state, &files_path.join("index.html"))
|
||||
path_to_response(&headers, &state, &files_path.join("index.html"))
|
||||
}
|
||||
}
|
||||
|
||||
fn path_to_response(headers: &HeaderMap, app_state: &AppState, path: &Path) -> Response {
|
||||
match path_to_response_(headers, app_state, path) {
|
||||
fn path_to_response(headers: &HeaderMap, state: &AppState, path: &Path) -> Response {
|
||||
match path_to_response_(headers, state, path) {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
let mut response =
|
||||
@@ -72,7 +72,7 @@ fn path_to_response(headers: &HeaderMap, app_state: &AppState, path: &Path) -> R
|
||||
}
|
||||
}
|
||||
|
||||
fn path_to_response_(headers: &HeaderMap, app_state: &AppState, path: &Path) -> Result<Response> {
|
||||
fn path_to_response_(headers: &HeaderMap, state: &AppState, path: &Path) -> Result<Response> {
|
||||
let (modified, date) = headers.check_if_modified_since(path)?;
|
||||
if modified == ModifiedState::NotModifiedSince {
|
||||
return Ok(Response::new_not_modified());
|
||||
@@ -86,7 +86,7 @@ fn path_to_response_(headers: &HeaderMap, app_state: &AppState, path: &Path) ->
|
||||
|| serialized_path.ends_with("service-worker.js");
|
||||
|
||||
let guard_res = if !must_revalidate {
|
||||
Some(app_state.cache.get_value_or_guard(
|
||||
Some(state.cache.get_value_or_guard(
|
||||
&path.to_str().unwrap().to_owned(),
|
||||
Some(Duration::from_millis(50)),
|
||||
))
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#![doc = include_str!("../examples/main.rs")]
|
||||
#![doc = "```"]
|
||||
|
||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
use std::{ops::Deref, path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
use aide::axum::ApiRouter;
|
||||
use api::ApiRoutes;
|
||||
@@ -42,6 +42,13 @@ pub struct AppState {
|
||||
cache: Arc<Cache<String, Bytes>>,
|
||||
}
|
||||
|
||||
impl Deref for AppState {
|
||||
type Target = &'static Interface<'static>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.interface
|
||||
}
|
||||
}
|
||||
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
pub struct Server(AppState);
|
||||
|
||||
@@ -4,7 +4,7 @@ use serde::Serialize;
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
/// Server health status
|
||||
pub struct Health {
|
||||
pub status: String,
|
||||
pub service: String,
|
||||
pub status: &'static str,
|
||||
pub service: &'static str,
|
||||
pub timestamp: String,
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ mod loadedaddressdata;
|
||||
mod loadedaddressindex;
|
||||
mod metriccount;
|
||||
mod metricpath;
|
||||
mod metricsearchquery;
|
||||
mod monthindex;
|
||||
mod ohlc;
|
||||
mod opreturnindex;
|
||||
@@ -116,6 +117,7 @@ pub use loadedaddressdata::*;
|
||||
pub use loadedaddressindex::*;
|
||||
pub use metriccount::*;
|
||||
pub use metricpath::*;
|
||||
pub use metricsearchquery::*;
|
||||
pub use monthindex::*;
|
||||
pub use ohlc::*;
|
||||
pub use opreturnindex::*;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
/// Search query parameters for finding metrics by name
|
||||
pub struct MetricSearchQuery {
|
||||
/// Search query string. Supports fuzzy matching, partial matches, and typos.
|
||||
#[schemars(example = &"price", example = &"low", example = &"sth", example = &"realized", example = &"pric")]
|
||||
pub q: String,
|
||||
|
||||
/// Maximum number of results to return. Defaults to 100 if not specified.
|
||||
#[serde(default = "default_search_limit")]
|
||||
#[schemars(
|
||||
example = "1",
|
||||
example = "10",
|
||||
example = "100",
|
||||
example = "1000",
|
||||
example = "10000",
|
||||
example = "100000"
|
||||
)]
|
||||
pub limit: usize,
|
||||
}
|
||||
|
||||
fn default_search_limit() -> usize {
|
||||
100
|
||||
}
|
||||
Reference in New Issue
Block a user