use std::collections::BTreeMap; use std::sync::{Arc, OnceLock}; use std::{env, process}; use serde::Deserialize; use trustfall::{execute_query, FieldValue, Schema, TransparentValue}; use crate::adapter::HackerNewsAdapter; pub mod adapter; mod util; pub mod vertex; static SCHEMA: OnceLock = OnceLock::new(); pub(crate) fn get_schema() -> &'static Schema { SCHEMA.get_or_init(|| { Schema::parse(util::read_file("./examples/hackernews/hackernews.graphql")) .expect("valid schema") }) } #[derive(Debug, Clone, Deserialize)] struct InputQuery<'a> { query: &'a str, args: BTreeMap, FieldValue>, } fn run_query(path: &str, max_results: Option) { let content = util::read_file(path); let input_query: InputQuery = ron::from_str(&content).unwrap(); let adapter = Arc::new(HackerNewsAdapter::new()); let schema = get_schema(); let query = input_query.query; let variables = input_query.args; for data_item in execute_query(schema, adapter, query, variables) .expect("not a legal query") .take(max_results.unwrap_or(usize::MAX)) { // The default `FieldValue` JSON representation is explicit about its type, so we can get // reliable round-trip serialization of types tricky in JSON like integers and floats. // // The `TransparentValue` type is like `FieldValue` minus the explicit type representation, // so it's more like what we'd expect to normally find in JSON. let transparent: BTreeMap<_, TransparentValue> = data_item.into_iter().map(|(k, v)| (k, v.into())).collect(); println!("\n{}", serde_json::to_string_pretty(&transparent).unwrap()); } } const USAGE: &str = "\ Commands: query [] - run the query in the given file over the HackerNews API optionally: fetching no more than Examples: (paths relative to `trustfall` crate directory) Links on the front page (as opposed to text submissions like \"Ask HN\"): cargo run --example hackernews query ./examples/hackernews/example_queries/front_page_stories_with_links.ron Latest links submitted by users with min 10000 karma cargo run --example hackernews query ./examples/hackernews/example_queries/latest_links_by_high_karma_users.ron patio11 commenting on his own blog posts cargo run --example hackernews query ./examples/hackernews/example_queries/patio11_comments_on_own_blog_posts.ron "; fn main() { let args: Vec = env::args().collect(); let mut reversed_args: Vec<_> = args.iter().map(|x| x.as_str()).rev().collect(); reversed_args .pop() .expect("Expected the executable name to be the first argument, but was missing"); match reversed_args.pop() { None => { println!("ERROR: no query file provided\n"); println!("{USAGE}"); process::exit(1); } Some("query") => match reversed_args.pop() { None => { println!("ERROR: no query file provided\n"); println!("{USAGE}"); process::exit(1); } Some(path) => { let max_results = reversed_args.pop().map(|value| match value.parse() { Ok(x) => x, Err(e) => { println!("ERROR: value for 'max_results' was not a number: {e}"); println!("{USAGE}"); process::exit(1); } }); if !reversed_args.is_empty() { println!("ERROR: unexpected arguments for 'query' command\n"); println!("{USAGE}"); process::exit(1); } run_query(path, max_results) } }, Some(cmd) => { println!("ERROR: unexpected command '{cmd}'\n"); println!("{USAGE}"); process::exit(1); } } }