1mod expression;
4
5pub use expression::{ExpressionError, SymbolExpression};
6
7use crate::data::{MaybeSend, Reply};
8use crate::model::{Instrument, SymbolType};
9
10pub trait SymbolSearch: MaybeSend + 'static {
43 fn search(&self, query: SymbolQuery, reply: Reply<Vec<Instrument>>);
45}
46
47#[derive(Clone, Debug, PartialEq)]
49#[non_exhaustive]
50pub struct SymbolQuery {
51 pub text: String,
53 pub symbol_type: Option<SymbolType>,
55 pub exchange: Option<String>,
57 pub offset: usize,
59 pub limit: usize,
61}
62
63#[cfg(feature = "http")]
64impl SymbolSearch for crate::data::http::HttpDatafeed {
65 fn search(&self, query: SymbolQuery, reply: Reply<Vec<Instrument>>) {
66 self.search_symbols(
67 query.text,
68 query.symbol_type,
69 query.exchange,
70 query.offset,
71 query.limit,
72 reply,
73 );
74 }
75}
76
77impl SymbolSearch for Vec<Instrument> {
82 fn search(&self, query: SymbolQuery, reply: Reply<Vec<Instrument>>) {
83 let text = query.text.to_lowercase();
84 let mut found: Vec<_> = self
85 .iter()
86 .filter(|instrument| {
87 (query.symbol_type.is_none() || query.symbol_type == instrument.symbol_type)
88 && (query.exchange.is_none() || query.exchange == instrument.exchange)
89 })
90 .filter_map(|instrument| {
91 let symbol = instrument.symbol.to_lowercase();
92 let described = || {
93 let description = instrument.description.as_deref().unwrap_or_default();
94 description.to_lowercase().contains(&text)
95 };
96 let rank = if symbol == text {
97 0
98 } else if symbol.starts_with(&text) {
99 1
100 } else if symbol.contains(&text) || described() {
101 2
102 } else {
103 return None;
104 };
105 Some((rank, instrument))
106 })
107 .collect();
108 found.sort_by_key(|&(rank, _)| rank);
109 let found = found.into_iter().skip(query.offset).take(query.limit);
110 reply.send(Ok(found
111 .map(|(_, instrument)| instrument.clone())
112 .collect()));
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::data::FeedError;
120 use crate::data::inbox::Inbox;
121 use std::sync::Arc;
122
123 #[test]
128 fn list_search_matches_and_filters() {
129 let list = vec![
130 Instrument::new("BTCUSD").with_symbol_type(SymbolType::Crypto),
131 Instrument::new("SPYG")
132 .with_description("SPDR Portfolio S&P 500 Growth ETF")
133 .with_symbol_type(SymbolType::Fund)
134 .with_exchange("NYSE"),
135 Instrument::new("SPY")
136 .with_description("SPDR S&P 500 ETF Trust")
137 .with_symbol_type(SymbolType::Fund)
138 .with_exchange("NYSE"),
139 Instrument::new("AAPL")
140 .with_description("Apple Inc.")
141 .with_symbol_type(SymbolType::Stock)
142 .with_exchange("NASDAQ"),
143 ];
144 let query = |text: &str, symbol_type, exchange: Option<&str>, offset, limit| SymbolQuery {
145 text: text.into(),
146 symbol_type,
147 exchange: exchange.map(Into::into),
148 limit,
149 offset,
150 };
151 let cases = [
152 (query("aapl", None, None, 0, 10), &["AAPL"][..]),
153 (query("APPLE", None, None, 0, 10), &["AAPL"]),
154 (query("s&p 500", None, None, 0, 10), &["SPYG", "SPY"]),
155 (query("s&p 500", None, None, 1, 10), &["SPY"]),
156 (query("spy", None, None, 2, 10), &[]),
157 (query("spy", None, None, 0, 10), &["SPY", "SPYG"]),
158 (query("spy", None, None, 0, 1), &["SPY"]),
159 (
160 query("", Some(SymbolType::Crypto), None, 0, 10),
161 &["BTCUSD"],
162 ),
163 (query("", None, Some("NASDAQ"), 0, 10), &["AAPL"]),
164 (query("s", None, None, 0, 10), &["SPYG", "SPY", "BTCUSD"]),
165 (query("xyz", None, None, 0, 10), &[]),
166 ];
167 for (query, expected) in cases {
168 let inbox = Arc::new(Inbox::default());
169 list.search(query.clone(), inbox.reply(0));
170 let answers = inbox.drain().answers;
171 assert_eq!(answers.len(), 1, "{query:?} not answered inline");
172 let (_, answer) = answers.into_iter().next().unwrap();
173 let found = answer
174 .downcast::<Result<Vec<Instrument>, FeedError>>()
175 .unwrap();
176 let symbols: Vec<_> = found.unwrap().into_iter().map(|i| i.symbol).collect();
177 assert_eq!(symbols, expected, "{query:?}");
178 }
179 }
180}