From 2cdbd7c7f5589cfa5ae5be373195b96df212cd39 Mon Sep 17 00:00:00 2001 From: Magnus Hoff Date: Wed, 25 Oct 2017 11:39:19 +0200 Subject: [PATCH] Use content type negotiation (the Accept header) to serve different formats from the _search endpoint --- src/models.rs | 2 +- src/resources/search_resource.rs | 64 ++++++++++++++++++++++++++------ src/site.rs | 7 +++- src/web/resource.rs | 5 +++ 4 files changed, 63 insertions(+), 15 deletions(-) diff --git a/src/models.rs b/src/models.rs index 130ea22..0d5a3af 100644 --- a/src/models.rs +++ b/src/models.rs @@ -33,7 +33,7 @@ pub struct ArticleRevisionStub { pub author: Option, } -#[derive(Debug, Queryable)] +#[derive(Debug, Queryable, Serialize)] pub struct SearchResult { pub title: String, pub snippet: String, diff --git a/src/resources/search_resource.rs b/src/resources/search_resource.rs index 60cb084..921346a 100644 --- a/src/resources/search_resource.rs +++ b/src/resources/search_resource.rs @@ -1,7 +1,8 @@ use futures::{self, Future}; use hyper; -use hyper::header::ContentType; +use hyper::header::{Accept, ContentType}; use hyper::server::*; +use serde_json; use serde_urlencoded; use assets::StyleCss; @@ -76,6 +77,7 @@ impl SearchLookup { pub struct SearchResource { state: State, + response_type: ResponseType, query: Option, limit: i32, @@ -83,9 +85,15 @@ pub struct SearchResource { snippet_size: i32, } +// This is a complete hack, searching for a reasonable design: +pub enum ResponseType { + Html, + Json, +} + impl SearchResource { pub fn new(state: State, query: Option, 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] } + // 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 { + 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() .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, } + #[derive(Serialize)] + struct JsonResponse<'a> { + query: &'a str, + hits: &'a [models::SearchResult], + } + impl models::SearchResult { fn link(&self) -> String { if self.slug == "" { @@ -128,16 +160,24 @@ impl Resource for SearchResource { Box::new(data.join(head) .and_then(move |(data, head)| { - Ok(head - .with_body(Layout { - base: None, // Hmm, should perhaps accept `base` as argument - title: "Search", - body: &Template { + match &self.response_type { + &ResponseType::Json => Ok(head + .with_body(serde_json::to_string(&JsonResponse { query: self.query.as_ref().map(|x| &**x).unwrap_or(""), - hits: data, - }, - style_css_checksum: StyleCss::checksum(), - }.to_string())) + hits: &data, + }).expect("Should never fail")) + ), + &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())), + } })) } } diff --git a/src/site.rs b/src/site.rs index 2b8fff3..cdff3e0 100644 --- a/src/site.rs +++ b/src/site.rs @@ -4,7 +4,7 @@ use std::fmt; use futures::{self, Future}; -use hyper::header::ContentType; +use hyper::header::{Accept, ContentType}; use hyper::mime; use hyper::server::*; use hyper; @@ -99,13 +99,16 @@ impl Service for Site { 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 base2 = base.clone(); // Bah, stupid clone Box::new(self.root.lookup(uri.path(), uri.query()) .and_then(move |resource| match resource { - Some(resource) => { + Some(mut resource) => { use hyper::Method::*; + resource.hacky_inject_accept_header(accept_header); match method { Options => Box::new(futures::finished(resource.options())), Head => resource.head(), diff --git a/src/web/resource.rs b/src/web/resource.rs index 37a0096..47b66ba 100644 --- a/src/web/resource.rs +++ b/src/web/resource.rs @@ -54,4 +54,9 @@ pub trait Resource { .with_header(header::ContentType(TEXT_PLAIN.clone())) .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. + } }