use crate::screen::Screen;
use crate::types::InlineResultKind;
#[derive(Debug, Clone)]
pub struct InlineResult {
pub id: String,
pub kind: InlineResultKind,
pub title: Option<String>,
pub description: Option<String>,
pub thumbnail_url: Option<String>,
pub screen: Option<Screen>,
}
impl InlineResult {
pub fn article(id: impl Into<String>) -> InlineResultBuilder {
InlineResultBuilder {
id: id.into(),
kind: InlineResultKind::Article,
title: None,
description: None,
thumbnail_url: None,
screen: None,
}
}
pub fn photo(id: impl Into<String>, url: impl Into<String>) -> InlineResultBuilder {
InlineResultBuilder {
id: id.into(),
kind: InlineResultKind::Photo { url: url.into() },
title: None,
description: None,
thumbnail_url: None,
screen: None,
}
}
pub fn gif(id: impl Into<String>, url: impl Into<String>) -> InlineResultBuilder {
InlineResultBuilder {
id: id.into(),
kind: InlineResultKind::Gif { url: url.into() },
title: None,
description: None,
thumbnail_url: None,
screen: None,
}
}
pub fn video(
id: impl Into<String>,
url: impl Into<String>,
mime: impl Into<String>,
) -> InlineResultBuilder {
InlineResultBuilder {
id: id.into(),
kind: InlineResultKind::Video {
url: url.into(),
mime: mime.into(),
},
title: None,
description: None,
thumbnail_url: None,
screen: None,
}
}
pub fn voice(id: impl Into<String>, url: impl Into<String>) -> InlineResultBuilder {
InlineResultBuilder {
id: id.into(),
kind: InlineResultKind::Voice { url: url.into() },
title: None,
description: None,
thumbnail_url: None,
screen: None,
}
}
pub fn document(
id: impl Into<String>,
url: impl Into<String>,
mime: impl Into<String>,
) -> InlineResultBuilder {
InlineResultBuilder {
id: id.into(),
kind: InlineResultKind::Document {
url: url.into(),
mime: mime.into(),
},
title: None,
description: None,
thumbnail_url: None,
screen: None,
}
}
}
pub struct InlineResultBuilder {
id: String,
kind: InlineResultKind,
title: Option<String>,
description: Option<String>,
thumbnail_url: Option<String>,
screen: Option<Screen>,
}
impl InlineResultBuilder {
pub fn title(mut self, t: impl Into<String>) -> Self {
self.title = Some(t.into());
self
}
pub fn description(mut self, d: impl Into<String>) -> Self {
self.description = Some(d.into());
self
}
pub fn thumb(mut self, url: impl Into<String>) -> Self {
self.thumbnail_url = Some(url.into());
self
}
pub fn screen(mut self, s: Screen) -> Self {
self.screen = Some(s);
self
}
pub fn build(self) -> InlineResult {
InlineResult {
id: self.id,
kind: self.kind,
title: self.title,
description: self.description,
thumbnail_url: self.thumbnail_url,
screen: self.screen,
}
}
}
pub struct InlineAnswer {
pub results: Vec<InlineResult>,
pub per_page: usize,
pub cache_time: i32,
pub is_personal: bool,
pub switch_pm_text: Option<String>,
pub switch_pm_parameter: Option<String>,
}
impl InlineAnswer {
pub fn new(results: Vec<InlineResult>) -> Self {
Self {
results,
per_page: 20,
cache_time: 300,
is_personal: false,
switch_pm_text: None,
switch_pm_parameter: None,
}
}
pub fn per_page(mut self, n: usize) -> Self {
self.per_page = n.clamp(1, 50);
self
}
pub fn cache_time(mut self, secs: i32) -> Self {
self.cache_time = secs;
self
}
pub fn personal(mut self) -> Self {
self.is_personal = true;
self
}
pub fn switch_pm(mut self, text: impl Into<String>, parameter: impl Into<String>) -> Self {
self.switch_pm_text = Some(text.into());
self.switch_pm_parameter = Some(parameter.into());
self
}
pub fn paginate(&self, raw_offset: &str) -> (Vec<&InlineResult>, String) {
let page: usize = if raw_offset.is_empty() {
0
} else {
raw_offset.parse().unwrap_or(0)
};
let start = page * self.per_page;
if start >= self.results.len() {
return (Vec::new(), String::new());
}
let end = (start + self.per_page).min(self.results.len());
let page_results: Vec<&InlineResult> = self.results[start..end].iter().collect();
let next_offset = if end < self.results.len() {
(page + 1).to_string()
} else {
String::new() };
(page_results, next_offset)
}
}
impl From<InlineResult> for crate::types::InlineQueryResult {
fn from(r: InlineResult) -> Self {
let (message_text, parse_mode, keyboard) = match r.screen {
Some(screen) => {
let msg = screen.messages.into_iter().next();
match msg.map(|m| m.content) {
Some(crate::types::MessageContent::Text {
text,
parse_mode,
keyboard,
..
}) => (Some(text), parse_mode, keyboard),
_ => (None, crate::types::ParseMode::Html, None),
}
}
None => (None, crate::types::ParseMode::Html, None),
};
crate::types::InlineQueryResult {
id: r.id,
kind: r.kind,
title: r.title,
description: r.description,
thumb_url: r.thumbnail_url,
message_text,
parse_mode,
keyboard,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_results(n: usize) -> Vec<InlineResult> {
(0..n)
.map(|i| {
InlineResult::article(i.to_string())
.title(format!("Result {}", i))
.build()
})
.collect()
}
#[test]
fn paginate_first_page() {
let answer = InlineAnswer::new(make_results(25)).per_page(10);
let (page, next) = answer.paginate("");
assert_eq!(page.len(), 10);
assert_eq!(page[0].id, "0");
assert_eq!(page[9].id, "9");
assert_eq!(next, "1");
}
#[test]
fn paginate_middle_page() {
let answer = InlineAnswer::new(make_results(25)).per_page(10);
let (page, next) = answer.paginate("1");
assert_eq!(page.len(), 10);
assert_eq!(page[0].id, "10");
assert_eq!(page[9].id, "19");
assert_eq!(next, "2");
}
#[test]
fn paginate_last_page() {
let answer = InlineAnswer::new(make_results(25)).per_page(10);
let (page, next) = answer.paginate("2");
assert_eq!(page.len(), 5);
assert_eq!(page[0].id, "20");
assert_eq!(page[4].id, "24");
assert!(next.is_empty(), "no more pages");
}
#[test]
fn paginate_past_end() {
let answer = InlineAnswer::new(make_results(25)).per_page(10);
let (page, next) = answer.paginate("100");
assert!(page.is_empty());
assert!(next.is_empty());
}
#[test]
fn paginate_empty_results() {
let answer = InlineAnswer::new(vec![]).per_page(10);
let (page, next) = answer.paginate("");
assert!(page.is_empty());
assert!(next.is_empty());
}
#[test]
fn paginate_exact_fit() {
let answer = InlineAnswer::new(make_results(20)).per_page(10);
let (page1, next1) = answer.paginate("");
assert_eq!(page1.len(), 10);
assert_eq!(next1, "1");
let (page2, next2) = answer.paginate("1");
assert_eq!(page2.len(), 10);
assert!(next2.is_empty(), "exactly 20 items in 2 pages of 10");
}
#[test]
fn paginate_single_page() {
let answer = InlineAnswer::new(make_results(5)).per_page(10);
let (page, next) = answer.paginate("");
assert_eq!(page.len(), 5);
assert!(next.is_empty());
}
#[test]
fn paginate_invalid_offset() {
let answer = InlineAnswer::new(make_results(25)).per_page(10);
let (page, next) = answer.paginate("not_a_number");
assert_eq!(page.len(), 10);
assert_eq!(page[0].id, "0");
assert_eq!(next, "1");
}
#[test]
fn per_page_clamped() {
let answer = InlineAnswer::new(make_results(100)).per_page(999);
assert_eq!(answer.per_page, 50);
let answer = InlineAnswer::new(make_results(100)).per_page(0);
assert_eq!(answer.per_page, 1);
}
#[test]
fn builder_article() {
let result = InlineResult::article("abc")
.title("Hello")
.description("World")
.thumb("https://example.com/thumb.jpg")
.build();
assert_eq!(result.id, "abc");
assert_eq!(result.title.as_deref(), Some("Hello"));
assert_eq!(result.description.as_deref(), Some("World"));
assert_eq!(
result.thumbnail_url.as_deref(),
Some("https://example.com/thumb.jpg")
);
assert!(matches!(result.kind, InlineResultKind::Article));
}
#[test]
fn builder_photo() {
let result = InlineResult::photo("p1", "https://example.com/photo.jpg")
.title("Photo")
.build();
assert_eq!(result.id, "p1");
assert!(
matches!(result.kind, InlineResultKind::Photo { ref url } if url == "https://example.com/photo.jpg")
);
}
#[test]
fn builder_gif() {
let result = InlineResult::gif("g1", "https://example.com/cat.gif").build();
assert!(
matches!(result.kind, InlineResultKind::Gif { ref url } if url == "https://example.com/cat.gif")
);
}
#[test]
fn builder_with_screen() {
let screen = Screen::text("test", "Hello from inline!").build();
let result = InlineResult::article("s1")
.title("With Screen")
.screen(screen)
.build();
assert!(result.screen.is_some());
}
#[test]
fn inline_answer_defaults() {
let answer = InlineAnswer::new(vec![]);
assert_eq!(answer.per_page, 20);
assert_eq!(answer.cache_time, 300);
assert!(!answer.is_personal);
assert!(answer.switch_pm_text.is_none());
}
#[test]
fn inline_answer_personal_and_switch_pm() {
let answer = InlineAnswer::new(vec![])
.personal()
.switch_pm("Start bot", "inline_ref");
assert!(answer.is_personal);
assert_eq!(answer.switch_pm_text.as_deref(), Some("Start bot"));
assert_eq!(answer.switch_pm_parameter.as_deref(), Some("inline_ref"));
}
}