use crate::client::Client;
use crate::error::Error;
use nordnet_model::models::instrument_search::{
AttributeResults, BullBearListResults, MinifutureListResults, OptionListResults,
StocklistResults, UnlimitedTurboListResults,
};
fn with_query(path: &str, qs: &str) -> String {
if qs.is_empty() {
path.to_owned()
} else {
format!("{path}?{qs}")
}
}
fn encode_pairs(pairs: &[(&str, Option<&str>)], multi: &[(&str, &[String])]) -> String {
let mut url = match reqwest::Url::parse("http://_/") {
Ok(u) => u,
Err(_) => return String::new(),
};
{
let mut q = url.query_pairs_mut();
for (name, value) in pairs {
if let Some(v) = value {
q.append_pair(name, v);
}
}
for (name, values) in multi {
if values.is_empty() {
continue;
}
q.append_pair(name, &values.join(","));
}
}
url.query().unwrap_or("").to_owned()
}
#[derive(Debug, Clone, Default)]
pub struct AttributesQuery<'a> {
pub apply_filters: Option<&'a str>,
pub attribute_group: Vec<String>,
pub entity_type: Option<&'a str>,
pub expand: Vec<String>,
pub minmax: Vec<String>,
pub only_filterable: Option<bool>,
pub only_returnable: Option<bool>,
pub only_sortable: Option<bool>,
}
fn build_attributes_query(q: &AttributesQuery<'_>) -> String {
let only_filterable = q.only_filterable.map(bool_str);
let only_returnable = q.only_returnable.map(bool_str);
let only_sortable = q.only_sortable.map(bool_str);
encode_pairs(
&[
("apply_filters", q.apply_filters),
("entity_type", q.entity_type),
("only_filterable", only_filterable),
("only_returnable", only_returnable),
("only_sortable", only_sortable),
],
&[
("attribute_group", &q.attribute_group),
("expand", &q.expand),
("minmax", &q.minmax),
],
)
}
fn bool_str(b: bool) -> &'static str {
if b {
"true"
} else {
"false"
}
}
#[derive(Debug, Clone, Default)]
pub struct StocklistQuery<'a> {
pub apply_filters: Option<&'a str>,
pub attribute_groups: Vec<String>,
pub attributes: Vec<String>,
pub free_text_search: Option<&'a str>,
pub limit: Option<i32>,
pub offset: Option<i32>,
pub sort_attribute: Option<&'a str>,
pub sort_order: Option<&'a str>,
}
fn build_stocklist_query(q: &StocklistQuery<'_>) -> String {
let limit = q.limit.map(|v| v.to_string());
let offset = q.offset.map(|v| v.to_string());
encode_pairs(
&[
("apply_filters", q.apply_filters),
("free_text_search", q.free_text_search),
("limit", limit.as_deref()),
("offset", offset.as_deref()),
("sort_attribute", q.sort_attribute),
("sort_order", q.sort_order),
],
&[
("attribute_groups", &q.attribute_groups),
("attributes", &q.attributes),
],
)
}
#[derive(Debug, Clone, Default)]
pub struct ListSearchQuery<'a> {
pub apply_filters: Option<&'a str>,
pub free_text_search: Option<&'a str>,
pub limit: Option<i32>,
pub offset: Option<i32>,
pub sort_attribute: Option<&'a str>,
pub sort_order: Option<&'a str>,
}
fn build_list_search_query(q: &ListSearchQuery<'_>) -> String {
let limit = q.limit.map(|v| v.to_string());
let offset = q.offset.map(|v| v.to_string());
encode_pairs(
&[
("apply_filters", q.apply_filters),
("free_text_search", q.free_text_search),
("limit", limit.as_deref()),
("offset", offset.as_deref()),
("sort_attribute", q.sort_attribute),
("sort_order", q.sort_order),
],
&[],
)
}
impl Client {
#[doc(alias = "GET /instrument_search/attributes")]
pub async fn get_attributes(
&self,
filters: AttributesQuery<'_>,
) -> Result<AttributeResults, Error> {
let qs = build_attributes_query(&filters);
let path = with_query("/instrument_search/attributes", &qs);
self.get::<AttributeResults>(&path).await
}
#[doc(alias = "GET /instrument_search/query/stocklist")]
pub async fn search_stocklist(
&self,
filters: StocklistQuery<'_>,
) -> Result<StocklistResults, Error> {
let qs = build_stocklist_query(&filters);
let path = with_query("/instrument_search/query/stocklist", &qs);
self.get::<StocklistResults>(&path).await
}
#[doc(alias = "GET /instrument_search/query/bullbearlist")]
pub async fn search_bullbearlist(
&self,
filters: ListSearchQuery<'_>,
) -> Result<BullBearListResults, Error> {
let qs = build_list_search_query(&filters);
let path = with_query("/instrument_search/query/bullbearlist", &qs);
match self.get::<BullBearListResults>(&path).await {
Ok(v) => Ok(v),
Err(Error::Decode { ref body, .. }) if body.trim().is_empty() => {
Ok(BullBearListResults {
results: None,
rows: None,
total_hits: None,
underlying_instrument_id: None,
})
}
Err(e) => Err(e),
}
}
#[doc(alias = "GET /instrument_search/query/minifuturelist")]
pub async fn search_minifuturelist(
&self,
filters: ListSearchQuery<'_>,
) -> Result<MinifutureListResults, Error> {
let qs = build_list_search_query(&filters);
let path = with_query("/instrument_search/query/minifuturelist", &qs);
self.get::<MinifutureListResults>(&path).await
}
#[doc(alias = "GET /instrument_search/query/unlimitedturbolist")]
pub async fn search_unlimitedturbolist(
&self,
filters: ListSearchQuery<'_>,
) -> Result<UnlimitedTurboListResults, Error> {
let qs = build_list_search_query(&filters);
let path = with_query("/instrument_search/query/unlimitedturbolist", &qs);
self.get::<UnlimitedTurboListResults>(&path).await
}
#[doc(alias = "GET /instrument_search/query/optionlist/pairs")]
pub async fn search_optionlist_pairs(
&self,
currency: &str,
expire_date: i64,
underlying_symbol: &str,
) -> Result<OptionListResults, Error> {
let expire_date = expire_date.to_string();
let qs = encode_pairs(
&[
("currency", Some(currency)),
("expire_date", Some(&expire_date)),
("underlying_symbol", Some(underlying_symbol)),
],
&[],
);
let path = with_query("/instrument_search/query/optionlist/pairs", &qs);
self.get::<OptionListResults>(&path).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn with_query_empty_omits_question_mark() {
assert_eq!(with_query("/x", ""), "/x");
assert_eq!(with_query("/x", "a=1"), "/x?a=1");
}
#[test]
fn attributes_query_empty_when_default() {
let qs = build_attributes_query(&AttributesQuery::default());
assert_eq!(qs, "");
}
#[test]
fn attributes_query_includes_scalar_and_repeated() {
let qs = build_attributes_query(&AttributesQuery {
apply_filters: Some("nordnet_markets=true"),
attribute_group: vec!["PRICE_INFO".to_owned(), "EXCHANGE_INFO".to_owned()],
entity_type: Some("STOCKLIST"),
expand: vec!["market_id".to_owned()],
minmax: vec![],
only_filterable: Some(true),
only_returnable: Some(false),
only_sortable: None,
});
assert_eq!(
qs,
"apply_filters=nordnet_markets%3Dtrue&entity_type=STOCKLIST&only_filterable=true&only_returnable=false&attribute_group=PRICE_INFO%2CEXCHANGE_INFO&expand=market_id"
);
}
#[test]
fn stocklist_query_default_empty() {
let qs = build_stocklist_query(&StocklistQuery::default());
assert_eq!(qs, "");
}
#[test]
fn stocklist_query_with_all_fields() {
let qs = build_stocklist_query(&StocklistQuery {
apply_filters: Some("instrument_type=ESH"),
attribute_groups: vec!["PRICE_INFO".to_owned()],
attributes: vec!["name".to_owned(), "isin".to_owned()],
free_text_search: Some("erics"),
limit: Some(25),
offset: Some(50),
sort_attribute: Some("name"),
sort_order: Some("desc"),
});
assert_eq!(
qs,
"apply_filters=instrument_type%3DESH&free_text_search=erics&limit=25&offset=50&sort_attribute=name&sort_order=desc&attribute_groups=PRICE_INFO&attributes=name%2Cisin"
);
}
#[test]
fn list_search_query_default_empty() {
let qs = build_list_search_query(&ListSearchQuery::default());
assert_eq!(qs, "");
}
#[test]
fn list_search_query_includes_pagination() {
let qs = build_list_search_query(&ListSearchQuery {
apply_filters: None,
free_text_search: Some("ERIC"),
limit: Some(10),
offset: Some(0),
sort_attribute: None,
sort_order: Some("asc"),
});
assert_eq!(qs, "free_text_search=ERIC&limit=10&offset=0&sort_order=asc");
}
}