#![allow(dead_code)]
use std::{collections::HashSet, sync::OnceLock};
use hn_api::{types::Item, HnClient};
use trustfall::{
provider::{
field_property, resolve_coercion_using_schema, resolve_neighbors_with,
resolve_property_with, BasicAdapter, ContextIterator, ContextOutcomeIterator,
EdgeParameters, VertexIterator,
},
FieldValue,
};
use trustfall_core::interpreter::AsVertex;
use crate::{get_schema, vertex::Vertex};
static CLIENT: OnceLock<HnClient> = OnceLock::new();
fn get_client() -> &'static HnClient {
CLIENT.get_or_init(|| HnClient::init().expect("HnClient instantiated"))
}
#[derive(Debug, Clone, Default)]
pub struct HackerNewsAdapter {
item_subtypes: HashSet<String>,
}
impl HackerNewsAdapter {
pub fn new() -> Self {
Self {
item_subtypes: get_schema()
.subtypes("Item")
.expect("Item type exists")
.map(|x| x.to_owned())
.collect(),
}
}
fn front_page<'a>(&self) -> VertexIterator<'a, Vertex> {
self.top(Some(30))
}
fn top<'a>(&self, max: Option<usize>) -> VertexIterator<'a, Vertex> {
let iterator = get_client()
.get_top_stories()
.unwrap()
.into_iter()
.take(max.unwrap_or(usize::MAX))
.filter_map(|id| match get_client().get_item(id) {
Ok(maybe_item) => maybe_item.map(|item| item.into()),
Err(e) => {
eprintln!("Got an error while fetching item: {e}");
None
}
});
Box::new(iterator)
}
fn latest_stories<'a>(&self, max: Option<usize>) -> VertexIterator<'a, Vertex> {
let story_ids: Vec<u32> =
reqwest::blocking::get("https://hacker-news.firebaseio.com/v0/newstories.json")
.unwrap()
.json()
.unwrap();
let iterator = story_ids
.into_iter()
.take(max.unwrap_or(usize::MAX))
.map(move |id| get_client().get_item(id))
.filter_map(|res| match res {
Ok(maybe_item) => maybe_item.map(|item| item.into()),
Err(e) => {
eprintln!("Got an error while fetching item: {e}");
None
}
});
Box::new(iterator)
}
fn user<'a>(&self, username: &str) -> VertexIterator<'a, Vertex> {
match get_client().get_user(username) {
Ok(Some(user)) => {
let vertex = Vertex::from(user);
Box::new(std::iter::once(vertex))
}
Ok(None) => {
Box::new(std::iter::empty())
}
Err(e) => {
eprintln!("Got an error while getting user profile for user {username}: {e}",);
Box::new(std::iter::empty())
}
}
}
}
macro_rules! item_property_resolver {
($attr:ident) => {
|vertex| -> FieldValue {
if let Some(s) = vertex.as_story() {
s.$attr.clone().into()
} else if let Some(j) = vertex.as_job() {
j.$attr.clone().into()
} else if let Some(c) = vertex.as_comment() {
c.$attr.clone().into()
} else if let Some(p) = vertex.as_poll() {
p.$attr.clone().into()
} else if let Some(p) = vertex.as_poll_option() {
p.$attr.clone().into()
} else {
unreachable!("{:?}", vertex)
}
}
};
}
impl<'a> BasicAdapter<'a> for HackerNewsAdapter {
type Vertex = Vertex;
fn resolve_starting_vertices(
&self,
edge_name: &str,
parameters: &EdgeParameters,
) -> VertexIterator<'a, Self::Vertex> {
match edge_name {
"FrontPage" => self.front_page(),
"Top" => {
let max = parameters.get("max").map(|v| v.as_u64().unwrap() as usize);
self.top(max)
}
"Latest" => {
let max = parameters.get("max").map(|v| v.as_u64().unwrap() as usize);
self.latest_stories(max)
}
"User" => {
let username_value = parameters["name"].as_str().unwrap();
self.user(username_value)
}
_ => unimplemented!("unexpected starting edge: {edge_name}"),
}
}
fn resolve_property<V: AsVertex<Self::Vertex> + 'a>(
&self,
contexts: ContextIterator<'a, V>,
type_name: &str,
property_name: &str,
) -> ContextOutcomeIterator<'a, V, FieldValue> {
match (type_name, property_name) {
(type_name, "id") if self.item_subtypes.contains(type_name) => {
resolve_property_with(contexts, item_property_resolver!(id))
}
(type_name, "unixTime") if self.item_subtypes.contains(type_name) => {
resolve_property_with(contexts, item_property_resolver!(time))
}
(type_name, "url") if self.item_subtypes.contains(type_name) => {
resolve_property_with(contexts, |vertex: &Vertex| {
let id = match vertex {
Vertex::Story(x) => x.id,
Vertex::Job(x) => x.id,
Vertex::Comment(x) => x.id,
Vertex::Poll(x) => x.id,
Vertex::PollOption(x) => x.id,
Vertex::User(_) => unreachable!("found a User which is not an Item"),
};
format!("https://news.ycombinator.com/item?id={id}").into()
})
}
("Job", "score") => resolve_property_with(contexts, field_property!(as_job, score)),
("Job", "title") => resolve_property_with(contexts, field_property!(as_job, title)),
("Job", "url") => resolve_property_with(contexts, field_property!(as_job, url)),
("Story", "byUsername") => {
resolve_property_with(contexts, field_property!(as_story, by))
}
("Story", "text") => resolve_property_with(contexts, field_property!(as_story, text)),
("Story", "commentsCount") => {
resolve_property_with(contexts, field_property!(as_story, descendants))
}
("Story", "score") => resolve_property_with(contexts, field_property!(as_story, score)),
("Story", "title") => resolve_property_with(contexts, field_property!(as_story, title)),
("Story", "submittedUrl") => {
resolve_property_with(contexts, field_property!(as_story, url))
}
("Comment", "byUsername") => {
resolve_property_with(contexts, field_property!(as_comment, by))
}
("Comment", "text") => {
resolve_property_with(contexts, field_property!(as_comment, text))
}
("Comment", "childCount") => resolve_property_with(
contexts,
field_property!(as_comment, kids, {
kids.as_ref().map(|v| v.len() as u64).unwrap_or(0).into()
}),
),
("User", "id") => resolve_property_with(contexts, field_property!(as_user, id)),
("User", "karma") => resolve_property_with(contexts, field_property!(as_user, karma)),
("User", "about") => resolve_property_with(contexts, field_property!(as_user, about)),
("User", "unixCreatedAt") => {
resolve_property_with(contexts, field_property!(as_user, created))
}
("User", "delay") => resolve_property_with(contexts, field_property!(as_user, delay)),
_ => unreachable!(),
}
}
fn resolve_neighbors<V: AsVertex<Self::Vertex> + 'a>(
&self,
contexts: ContextIterator<'a, V>,
type_name: &str,
edge_name: &str,
_parameters: &EdgeParameters,
) -> ContextOutcomeIterator<'a, V, VertexIterator<'a, Self::Vertex>> {
match (type_name, edge_name) {
("Story", "byUser") => {
let edge_resolver = |vertex: &Self::Vertex| -> VertexIterator<'a, Self::Vertex> {
let story = vertex.as_story().unwrap();
let author = story.by.as_str();
match get_client().get_user(author) {
Ok(None) => Box::new(std::iter::empty()), Ok(Some(user)) => Box::new(std::iter::once(user.into())),
Err(e) => {
eprintln!(
"API error while fetching story {} author \"{}\": {}",
story.id, author, e
);
Box::new(std::iter::empty())
}
}
};
resolve_neighbors_with(contexts, edge_resolver)
}
("Story", "comment") => {
let edge_resolver = |vertex: &Self::Vertex| {
let story = vertex.as_story().unwrap();
let comment_ids = story.kids.clone().unwrap_or_default();
let story_id = story.id;
let neighbors: VertexIterator<'a, Self::Vertex> =
Box::new(comment_ids.into_iter().filter_map(move |comment_id| {
match get_client().get_item(comment_id) {
Ok(None) => None,
Ok(Some(item)) => {
if let Item::Comment(comment) = item {
Some(comment.into())
} else {
unreachable!()
}
}
Err(e) => {
eprintln!(
"API error while fetching story {story_id} comment {comment_id}: {e}",
);
None
}
}
}));
neighbors
};
resolve_neighbors_with(contexts, edge_resolver)
}
("Comment", "byUser") => {
let edge_resolver = |vertex: &Self::Vertex| {
let comment = vertex.as_comment().unwrap();
let author = comment.by.as_str();
let neighbors: VertexIterator<'a, Self::Vertex> =
match get_client().get_user(author) {
Ok(None) => Box::new(std::iter::empty()), Ok(Some(user)) => Box::new(std::iter::once(user.into())),
Err(e) => {
eprintln!(
"API error while fetching comment {} author \"{}\": {}",
comment.id, author, e
);
Box::new(std::iter::empty())
}
};
neighbors
};
resolve_neighbors_with(contexts, edge_resolver)
}
("Comment", "parent") => {
let edge_resolver = |vertex: &Self::Vertex| {
let comment = vertex.as_comment().unwrap();
let comment_id = comment.id;
let parent_id = comment.parent;
let neighbors: VertexIterator<'a, Self::Vertex> = match get_client()
.get_item(parent_id)
{
Ok(None) => Box::new(std::iter::empty()),
Ok(Some(item)) => Box::new(std::iter::once(item.into())),
Err(e) => {
eprintln!(
"API error while fetching comment {comment_id} parent {parent_id}: {e}",
);
Box::new(std::iter::empty())
}
};
neighbors
};
resolve_neighbors_with(contexts, edge_resolver)
}
("Comment", "reply") => {
let edge_resolver = |vertex: &Self::Vertex| {
let comment = vertex.as_comment().unwrap();
let comment_id = comment.id;
let reply_ids = comment.kids.clone().unwrap_or_default();
let neighbors: VertexIterator<'a, Self::Vertex> = Box::new(reply_ids.into_iter().filter_map(move |reply_id| {
match get_client().get_item(reply_id) {
Ok(None) => None,
Ok(Some(item)) => {
if let Item::Comment(c) = item {
Some(c.into())
} else {
unreachable!()
}
}
Err(e) => {
eprintln!(
"API error while fetching comment {comment_id} reply {reply_id}: {e}",
);
None
}
}
}));
neighbors
};
resolve_neighbors_with(contexts, edge_resolver)
}
("User", "submitted") => {
let edge_resolver = |vertex: &Self::Vertex| {
let user = vertex.as_user().unwrap();
let submitted_ids = user.submitted.clone();
let neighbors: VertexIterator<'a, Self::Vertex> =
Box::new(submitted_ids.into_iter().filter_map(move |submission_id| {
match get_client().get_item(submission_id) {
Ok(None) => None,
Ok(Some(item)) => Some(item.into()),
Err(e) => {
eprintln!(
"API error while fetching submitted item {submission_id}: {e}",
);
None
}
}
}));
neighbors
};
resolve_neighbors_with(contexts, edge_resolver)
}
_ => unreachable!("{} {}", type_name, edge_name),
}
}
fn resolve_coercion<V: AsVertex<Self::Vertex> + 'a>(
&self,
contexts: ContextIterator<'a, V>,
_type_name: &str,
coerce_to_type: &str,
) -> ContextOutcomeIterator<'a, V, bool> {
resolve_coercion_using_schema(contexts, get_schema(), coerce_to_type)
}
}