use std::fmt::Display;
#[derive(Debug, Default)]
pub(crate) struct QueryBuilder {
pairs: Vec<(&'static str, String)>,
}
impl QueryBuilder {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn push(&mut self, key: &'static str, value: impl Display) {
self.pairs.push((key, value.to_string()));
}
pub(crate) fn push_opt(&mut self, key: &'static str, value: Option<impl Display>) {
if let Some(value) = value {
self.push(key, value);
}
}
pub(crate) fn push_flag(&mut self, key: &'static str, value: Option<bool>) {
self.push_opt(key, value);
}
pub(crate) fn push_each(
&mut self,
key: &'static str,
values: impl IntoIterator<Item = String>,
) {
for value in values {
self.pairs.push((key, value));
}
}
pub(crate) fn pairs(&self) -> Vec<(&str, &str)> {
self.pairs
.iter()
.map(|(key, value)| (*key, value.as_str()))
.collect()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PageRequest {
page_offset: Option<u32>,
per_page: Option<u32>,
}
impl PageRequest {
pub fn new() -> Self {
Self::default()
}
pub fn first() -> Self {
Self::new().with_page_offset(0)
}
#[must_use]
pub fn with_page_offset(mut self, page_offset: u32) -> Self {
self.page_offset = Some(page_offset);
self
}
#[must_use]
pub fn with_per_page(mut self, per_page: u32) -> Self {
self.per_page = Some(per_page);
self
}
pub fn page_offset(&self) -> Option<u32> {
self.page_offset
}
pub fn per_page(&self) -> Option<u32> {
self.per_page
}
#[must_use]
pub fn next_page(self) -> Self {
Self {
page_offset: Some(self.page_offset.unwrap_or(0).saturating_add(1)),
..self
}
}
pub(crate) fn write_into(&self, query: &mut QueryBuilder) {
query.push_opt("page-offset", self.page_offset);
query.push_opt("per-page", self.per_page);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_absent_optional_parameter_is_absent_rather_than_empty() {
let mut query = QueryBuilder::new();
query.push_opt("per-page", None::<u32>);
query.push_flag("is-etf", None);
assert!(query.pairs().is_empty());
}
#[test]
fn an_array_parameter_repeats_its_key() {
let mut query = QueryBuilder::new();
query.push_each(
"product-code[]",
["ES".to_string(), "6A".to_string(), "CL".to_string()],
);
assert_eq!(
query.pairs(),
vec![
("product-code[]", "ES"),
("product-code[]", "6A"),
("product-code[]", "CL"),
]
);
}
#[test]
fn an_empty_array_parameter_adds_nothing() {
let mut query = QueryBuilder::new();
query.push_each("symbol[]", Vec::new());
assert!(query.pairs().is_empty());
}
#[test]
fn a_flag_renders_as_true_or_false() {
let mut query = QueryBuilder::new();
query.push_flag("is-etf", Some(true));
query.push_flag("is-index", Some(false));
assert_eq!(
query.pairs(),
vec![("is-etf", "true"), ("is-index", "false")]
);
}
#[test]
fn parameters_keep_the_order_they_were_added_in() {
let mut query = QueryBuilder::new();
query.push("a", 1);
query.push_each("b[]", ["x".to_string()]);
query.push("c", "z");
assert_eq!(query.pairs(), vec![("a", "1"), ("b[]", "x"), ("c", "z")]);
}
#[test]
fn a_default_page_request_sends_nothing() {
let mut query = QueryBuilder::new();
PageRequest::default().write_into(&mut query);
assert!(
query.pairs().is_empty(),
"an unset page must leave the venue's own defaults alone"
);
}
#[test]
fn a_page_request_sends_only_what_it_was_given() {
let mut query = QueryBuilder::new();
PageRequest::new().with_per_page(50).write_into(&mut query);
assert_eq!(query.pairs(), vec![("per-page", "50")]);
}
#[test]
fn the_next_page_keeps_the_size_and_advances_the_offset() {
let page = PageRequest::new().with_per_page(25).with_page_offset(3);
let next = page.next_page();
assert_eq!(next.page_offset(), Some(4));
assert_eq!(next.per_page(), Some(25));
assert_eq!(PageRequest::new().next_page().page_offset(), Some(1));
}
#[test]
fn advancing_past_the_end_saturates() {
let page = PageRequest::new().with_page_offset(u32::MAX);
assert_eq!(page.next_page().page_offset(), Some(u32::MAX));
}
}