use std::collections::BTreeMap;
use std::future::Future;
use std::sync::Arc;
use axum::Router;
use axum::extract::State;
use axum::response::IntoResponse;
use axum::routing::get;
use http::StatusCode;
use jsonapi_axum::{
ApiErrorExt, DocumentBuilder, IncludeResolver, IntoJsonApiError, JsonApiError, JsonApiLayer,
JsonApiQuery, JsonApiResponse, ResultExt, resolve_includes, with_status,
};
use jsonapi_core::{Identity, Relationship, RelationshipData, Resource, ResourceIdentifier};
#[derive(Debug, Clone, jsonapi_core::JsonApi)]
#[jsonapi(type = "people")]
struct Person {
#[jsonapi(id)]
id: String,
name: String,
}
#[derive(Debug, Clone, jsonapi_core::JsonApi)]
#[jsonapi(type = "articles")]
struct Article {
#[jsonapi(id)]
id: String,
title: String,
#[jsonapi(relationship)]
author: Relationship<Person>,
}
#[derive(Clone)]
struct AppState {
articles: Arc<Vec<Article>>,
people: Arc<BTreeMap<String, Person>>,
}
impl AppState {
fn resolver(&self) -> PeopleResolver {
PeopleResolver {
people: self.people.clone(),
}
}
}
struct PeopleResolver {
people: Arc<BTreeMap<String, Person>>,
}
#[derive(Debug)]
struct ResolveError {
type_name: String,
}
impl IntoJsonApiError for ResolveError {
fn into_json_api_error(self) -> JsonApiError {
JsonApiError::from_api_error(with_status(StatusCode::INTERNAL_SERVER_ERROR).detail(
format!("cannot resolve includes of type `{}`", self.type_name),
))
}
}
impl IncludeResolver for PeopleResolver {
type Error = ResolveError;
fn load(
&self,
type_name: &str,
ids: &[String],
) -> impl Future<Output = Result<Vec<Resource>, Self::Error>> + Send {
let result = match type_name {
"people" => ids
.iter()
.filter_map(|id| self.people.get(id))
.map(|person| {
Resource::from_typed(person).map_err(|_| ResolveError {
type_name: "people".to_string(),
})
})
.collect::<Result<Vec<_>, _>>(),
other => Err(ResolveError {
type_name: other.to_string(),
}),
};
async move { result }
}
}
async fn list_articles(
State(state): State<AppState>,
JsonApiQuery(query): JsonApiQuery,
) -> Result<impl IntoResponse, JsonApiError> {
let articles = (*state.articles).clone();
let primary: Vec<Resource> = articles
.iter()
.map(Resource::from_typed)
.collect::<Result<_, _>>()
.map_err(|err| JsonApiError::from_core(&err))?;
let paths: Vec<&str> = query.include.iter().map(String::as_str).collect();
let included = resolve_includes(&primary, &paths, &state.resolver())
.await
.or_json_api()?;
let document = DocumentBuilder::collection(articles)
.include_many(included)
.build();
Ok(JsonApiResponse::new(document))
}
fn author(id: &str) -> Relationship<Person> {
Relationship::new(RelationshipData::ToOne(Some(ResourceIdentifier {
r#type: "people".to_string(),
identity: Identity::Id(id.to_string()),
meta: None,
})))
}
fn app() -> Router {
let people = BTreeMap::from([(
"9".to_string(),
Person {
id: "9".to_string(),
name: "Dan Gebhardt".to_string(),
},
)]);
let articles = vec![
Article {
id: "1".to_string(),
title: "JSON:API paints my bikeshed!".to_string(),
author: author("9"),
},
Article {
id: "2".to_string(),
title: "Rails is Omakase".to_string(),
author: author("9"),
},
];
let state = AppState {
articles: Arc::new(articles),
people: Arc::new(people),
};
Router::new()
.route("/articles", get(list_articles))
.layer(JsonApiLayer::new())
.with_state(state)
}
#[tokio::main]
async fn main() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:3001")
.await
.expect("bind 127.0.0.1:3001");
println!(
"JSON:API compound-document server listening on http://{}",
listener.local_addr().unwrap()
);
axum::serve(listener, app()).await.expect("server error");
}