Use content type negotiation (the Accept header) to serve different formats from the _search endpoint
This commit is contained in:
parent
313fc16add
commit
2cdbd7c7f5
4 changed files with 63 additions and 15 deletions
|
@ -33,7 +33,7 @@ pub struct ArticleRevisionStub {
|
||||||
pub author: Option<String>,
|
pub author: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Queryable)]
|
#[derive(Debug, Queryable, Serialize)]
|
||||||
pub struct SearchResult {
|
pub struct SearchResult {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub snippet: String,
|
pub snippet: String,
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use futures::{self, Future};
|
use futures::{self, Future};
|
||||||
use hyper;
|
use hyper;
|
||||||
use hyper::header::ContentType;
|
use hyper::header::{Accept, ContentType};
|
||||||
use hyper::server::*;
|
use hyper::server::*;
|
||||||
|
use serde_json;
|
||||||
use serde_urlencoded;
|
use serde_urlencoded;
|
||||||
|
|
||||||
use assets::StyleCss;
|
use assets::StyleCss;
|
||||||
|
@ -76,6 +77,7 @@ impl SearchLookup {
|
||||||
|
|
||||||
pub struct SearchResource {
|
pub struct SearchResource {
|
||||||
state: State,
|
state: State,
|
||||||
|
response_type: ResponseType,
|
||||||
|
|
||||||
query: Option<String>,
|
query: Option<String>,
|
||||||
limit: i32,
|
limit: i32,
|
||||||
|
@ -83,9 +85,15 @@ pub struct SearchResource {
|
||||||
snippet_size: i32,
|
snippet_size: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is a complete hack, searching for a reasonable design:
|
||||||
|
pub enum ResponseType {
|
||||||
|
Html,
|
||||||
|
Json,
|
||||||
|
}
|
||||||
|
|
||||||
impl SearchResource {
|
impl SearchResource {
|
||||||
pub fn new(state: State, query: Option<String>, limit: i32, offset: i32, snippet_size: i32) -> Self {
|
pub fn new(state: State, query: Option<String>, limit: i32, offset: i32, snippet_size: i32) -> Self {
|
||||||
Self { state, query, limit, offset, snippet_size }
|
Self { state, response_type: ResponseType::Html, query, limit, offset, snippet_size }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,10 +103,28 @@ impl Resource for SearchResource {
|
||||||
vec![Options, Head, Get]
|
vec![Options, Head, Get]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is a complete hack, searching for a reasonable design:
|
||||||
|
fn hacky_inject_accept_header(&mut self, accept: Accept) {
|
||||||
|
use hyper::header::QualityItem;
|
||||||
|
use hyper::mime;
|
||||||
|
|
||||||
|
self.response_type = match accept.first() {
|
||||||
|
Some(&QualityItem { item: ref mime, .. })
|
||||||
|
if mime.type_() == mime::APPLICATION && mime.subtype() == mime::JSON
|
||||||
|
=> ResponseType::Json,
|
||||||
|
_ => ResponseType::Html,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
fn head(&self) -> ResponseFuture {
|
fn head(&self) -> ResponseFuture {
|
||||||
|
let content_type = match &self.response_type {
|
||||||
|
&ResponseType::Json => ContentType(APPLICATION_JSON.clone()),
|
||||||
|
&ResponseType::Html => ContentType(TEXT_HTML.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
Box::new(futures::finished(Response::new()
|
Box::new(futures::finished(Response::new()
|
||||||
.with_status(hyper::StatusCode::Ok)
|
.with_status(hyper::StatusCode::Ok)
|
||||||
.with_header(ContentType(TEXT_HTML.clone()))
|
.with_header(content_type)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,6 +136,12 @@ impl Resource for SearchResource {
|
||||||
hits: Vec<models::SearchResult>,
|
hits: Vec<models::SearchResult>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct JsonResponse<'a> {
|
||||||
|
query: &'a str,
|
||||||
|
hits: &'a [models::SearchResult],
|
||||||
|
}
|
||||||
|
|
||||||
impl models::SearchResult {
|
impl models::SearchResult {
|
||||||
fn link(&self) -> String {
|
fn link(&self) -> String {
|
||||||
if self.slug == "" {
|
if self.slug == "" {
|
||||||
|
@ -128,16 +160,24 @@ impl Resource for SearchResource {
|
||||||
|
|
||||||
Box::new(data.join(head)
|
Box::new(data.join(head)
|
||||||
.and_then(move |(data, head)| {
|
.and_then(move |(data, head)| {
|
||||||
Ok(head
|
match &self.response_type {
|
||||||
.with_body(Layout {
|
&ResponseType::Json => Ok(head
|
||||||
base: None, // Hmm, should perhaps accept `base` as argument
|
.with_body(serde_json::to_string(&JsonResponse {
|
||||||
title: "Search",
|
|
||||||
body: &Template {
|
|
||||||
query: self.query.as_ref().map(|x| &**x).unwrap_or(""),
|
query: self.query.as_ref().map(|x| &**x).unwrap_or(""),
|
||||||
hits: data,
|
hits: &data,
|
||||||
},
|
}).expect("Should never fail"))
|
||||||
style_css_checksum: StyleCss::checksum(),
|
),
|
||||||
}.to_string()))
|
&ResponseType::Html => Ok(head
|
||||||
|
.with_body(Layout {
|
||||||
|
base: None, // Hmm, should perhaps accept `base` as argument
|
||||||
|
title: "Search",
|
||||||
|
body: &Template {
|
||||||
|
query: self.query.as_ref().map(|x| &**x).unwrap_or(""),
|
||||||
|
hits: data,
|
||||||
|
},
|
||||||
|
style_css_checksum: StyleCss::checksum(),
|
||||||
|
}.to_string())),
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use futures::{self, Future};
|
use futures::{self, Future};
|
||||||
use hyper::header::ContentType;
|
use hyper::header::{Accept, ContentType};
|
||||||
use hyper::mime;
|
use hyper::mime;
|
||||||
use hyper::server::*;
|
use hyper::server::*;
|
||||||
use hyper;
|
use hyper;
|
||||||
|
@ -99,13 +99,16 @@ impl Service for Site {
|
||||||
false => None,
|
false => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let accept_header = headers.get().map(|x: &Accept| x.clone()).unwrap_or(Accept(vec![]));
|
||||||
|
|
||||||
let base = root_base_from_request_uri(uri.path());
|
let base = root_base_from_request_uri(uri.path());
|
||||||
let base2 = base.clone(); // Bah, stupid clone
|
let base2 = base.clone(); // Bah, stupid clone
|
||||||
|
|
||||||
Box::new(self.root.lookup(uri.path(), uri.query())
|
Box::new(self.root.lookup(uri.path(), uri.query())
|
||||||
.and_then(move |resource| match resource {
|
.and_then(move |resource| match resource {
|
||||||
Some(resource) => {
|
Some(mut resource) => {
|
||||||
use hyper::Method::*;
|
use hyper::Method::*;
|
||||||
|
resource.hacky_inject_accept_header(accept_header);
|
||||||
match method {
|
match method {
|
||||||
Options => Box::new(futures::finished(resource.options())),
|
Options => Box::new(futures::finished(resource.options())),
|
||||||
Head => resource.head(),
|
Head => resource.head(),
|
||||||
|
|
|
@ -54,4 +54,9 @@ pub trait Resource {
|
||||||
.with_header(header::ContentType(TEXT_PLAIN.clone()))
|
.with_header(header::ContentType(TEXT_PLAIN.clone()))
|
||||||
.with_body("Method not allowed\n")
|
.with_body("Method not allowed\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn hacky_inject_accept_header(&mut self, _: header::Accept) {
|
||||||
|
// This function is a complete hack, searching for the appropriate
|
||||||
|
// architecture.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue