summaryrefslogtreecommitdiffstats
path: root/src/stackexchange.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/stackexchange.rs')
-rw-r--r--src/stackexchange.rs111
1 files changed, 90 insertions, 21 deletions
diff --git a/src/stackexchange.rs b/src/stackexchange.rs
index ddc3c48..1d4789a 100644
--- a/src/stackexchange.rs
+++ b/src/stackexchange.rs
@@ -1,4 +1,5 @@
use futures::stream::StreamExt;
+use rayon::prelude::*;
use reqwest::Client;
use reqwest::Url;
use serde::{Deserialize, Serialize};
@@ -8,6 +9,8 @@ use std::path::PathBuf;
use crate::config::{project_dir, Config};
use crate::error::{Error, Result};
+use crate::tui::markdown;
+use crate::tui::markdown::Markdown;
use crate::utils;
/// StackExchange API v2.2 URL
@@ -50,12 +53,12 @@ pub struct Site {
/// Represents a StackExchange answer with a custom selection of fields from
/// the [StackExchange docs](https://api.stackexchange.com/docs/types/answer)
#[derive(Clone, Deserialize, Debug)]
-pub struct Answer {
+pub struct Answer<S> {
#[serde(rename = "answer_id")]
pub id: u32,
pub score: i32,
#[serde(rename = "body_markdown")]
- pub body: String,
+ pub body: S,
pub is_accepted: bool,
}
@@ -64,14 +67,14 @@ pub struct Answer {
// TODO container over answers should be generic iterator
// TODO let body be a generic that implements Display!
#[derive(Clone, Deserialize, Debug)]
-pub struct Question {
+pub struct Question<S> {
#[serde(rename = "question_id")]
pub id: u32,
pub score: i32,
- pub answers: Vec<Answer>,
+ pub answers: Vec<Answer<S>>,
pub title: String,
#[serde(rename = "body_markdown")]
- pub body: String,
+ pub body: S,
}
/// Internal struct that represents the boilerplate response wrapper from SE API.
@@ -110,12 +113,12 @@ impl StackExchange {
}
/// Search query at stack exchange and get a list of relevant questions
- pub async fn search(&self) -> Result<Vec<Question>> {
+ pub async fn search(&self) -> Result<Vec<Question<Markdown>>> {
self.search_advanced(self.config.limit).await
}
/// Parallel searches against the search/advanced endpoint across all configured sites
- async fn search_advanced(&self, limit: u16) -> Result<Vec<Question>> {
+ async fn search_advanced(&self, limit: u16) -> Result<Vec<Question<Markdown>>> {
futures::stream::iter(self.config.sites.clone())
.map(|site| {
let clone = self.clone();
@@ -131,18 +134,18 @@ impl StackExchange {
.map(|r| r.map_err(Error::from).and_then(|x| x))
.collect::<Result<Vec<Vec<_>>>>()
.map(|v| {
- let mut all_qs: Vec<Question> = v.into_iter().flatten().collect();
+ let mut qs: Vec<Question<String>> = v.into_iter().flatten().collect();
if self.config.sites.len() > 1 {
- all_qs.sort_unstable_by_key(|q| -q.score);
+ qs.sort_unstable_by_key(|q| -q.score);
}
- all_qs
+ Self::parse_markdown(qs)
})
}
/// Search against the site's search/advanced endpoint with a given query.
/// Only fetches questions that have at least one answer.
- async fn search_advanced_site(&self, site: &str, limit: u16) -> Result<Vec<Question>> {
- Ok(self
+ async fn search_advanced_site(&self, site: &str, limit: u16) -> Result<Vec<Question<String>>> {
+ let qs = self
.client
.get(stackexchange_url("search/advanced"))
.header("Accepts", "application/json")
@@ -158,16 +161,10 @@ impl StackExchange {
])
.send()
.await?
- .json::<ResponseWrapper<Question>>()
+ .json::<ResponseWrapper<Question<String>>>()
.await?
- .items
- .into_iter()
- .map(|mut q| {
- // TODO parallelize this (and preprocess <kbd> stuff too)
- q.answers.sort_unstable_by_key(|a| -a.score);
- q
- })
- .collect())
+ .items;
+ Ok(Self::preprocess(qs))
}
fn get_default_opts(&self) -> HashMap<&str, &str> {
@@ -178,6 +175,78 @@ impl StackExchange {
}
params
}
+
+ /// Sorts answers by score
+ /// Preprocess SE markdown to "cmark" markdown (or something closer to it)
+ fn preprocess(qs: Vec<Question<String>>) -> Vec<Question<String>> {
+ qs.par_iter()
+ .map(|q| {
+ let Question {
+ id,
+ score,
+ title,
+ answers,
+ body,
+ } = q;
+ answers.to_vec().par_sort_unstable_by_key(|a| -a.score);
+ let answers = answers
+ .par_iter()
+ .map(|a| Answer {
+ body: markdown::preprocess(a.body.clone()),
+ ..*a
+ })
+ .collect();
+ Question {
+ answers,
+ body: markdown::preprocess(body.to_string()),
+ id: *id,
+ score: *score,
+ title: title.to_string(),
+ }
+ })
+ .collect::<Vec<_>>()
+ }
+
+ /// Parse all markdown fields
+ fn parse_markdown(qs: Vec<Question<String>>) -> Vec<Question<Markdown>> {
+ qs.par_iter()
+ .map(|q| {
+ let Question {
+ id,
+ score,
+ title,
+ answers,
+ body,
+ } = q;
+ let body = markdown::parse(body);
+ let answers = answers
+ .par_iter()
+ .map(|a| {
+ let Answer {
+ id,
+ score,
+ is_accepted,
+ body,
+ } = a;
+ let body = markdown::parse(body);
+ Answer {
+ body,
+ id: *id,
+ score: *score,
+ is_accepted: *is_accepted,
+ }
+ })
+ .collect::<Vec<_>>();
+ Question {
+ body,
+ answers,
+ id: *id,
+ score: *score,
+ title: title.to_string(),
+ }
+ })
+ .collect::<Vec<_>>()
+ }
}
impl LocalStorage {