Getting Started

The reference implementation will serve queries and mutations based on in-memory data read from newline-delimited JSON files.

First, we will define some types to represent the data in the newline-delimited JSON files. Rows of JSON data will be stored in memory as ordered maps:

type Row = BTreeMap<models::FieldName, serde_json::Value>;

Our application state will consist of collections of various types of rows:

#[derive(Debug, Clone)]
pub struct AppState {
    pub articles: BTreeMap<i32, Row>,
    pub authors: BTreeMap<i32, Row>,
    pub institutions: BTreeMap<i32, Row>,
    pub metrics: Metrics,
}

In our main function, the data connector reads the initial data from the newline-delimited JSON files, and creates the AppState:

fn init_app_state() -> AppState {
    // Read the JSON data files
    let articles = read_json_lines(ARTICLES_JSON).unwrap();
    let authors = read_json_lines(AUTHORS_JSON).unwrap();
    let institutions = read_json_lines(INSTITUTIONS_JSON).unwrap();

    let metrics = Metrics::new().unwrap();

    AppState {
        articles,
        authors,
        institutions,
        metrics,
    }
}

Finally, we start a web server with the endpoints which are required by this specification:

#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn Error>> {
    let app_state = Arc::new(Mutex::new(init_app_state()));

    let app = Router::new()
        .route("/health", get(get_health))
        .route("/metrics", get(get_metrics))
        .route("/capabilities", get(get_capabilities))
        .route("/schema", get(get_schema))
        .route("/query", post(post_query))
        .route("/query/explain", post(post_query_explain))
        .route("/mutation", post(post_mutation))
        .route("/mutation/explain", post(post_mutation_explain))
        .layer(axum::middleware::from_fn_with_state(
            app_state.clone(),
            metrics_middleware,
        ))
        .with_state(app_state);

    // Start the server on `localhost:<PORT>`.
    // This says it's binding to an IPv6 address, but will actually listen to
    // any IPv4 or IPv6 address.
    let host = net::IpAddr::V6(net::Ipv6Addr::UNSPECIFIED);
    let port = env::var("PORT")
        .map(|s| s.parse())
        .unwrap_or(Ok(DEFAULT_PORT))?;
    let addr = net::SocketAddr::new(host, port);

    let server = axum::Server::bind(&addr).serve(app.into_make_service());
    println!("Serving on {}", server.local_addr());
    server.with_graceful_shutdown(shutdown_handler()).await?;

    Ok(())
}

Note: the application state is stored in an Arc<Mutex<_>>, so that we can perform locking reads and writes in multiple threads.

In the next chapters, we will look at the implementation of each of these endpoints in turn.