From 9048fe62c4ba5188c4b1c1922d97730c347ff8a2 Mon Sep 17 00:00:00 2001 From: Ben Bangert Date: Fri, 30 Nov 2018 12:47:00 -0800 Subject: [PATCH] feat: return newlines reply when get accept header indicates it Closes #99 --- src/error.rs | 1 + src/web/error.rs | 9 ++++++--- src/web/extractors.rs | 23 ++++++++++++++++++++++- src/web/handlers.rs | 25 +++++++++++++++++++++---- 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/error.rs b/src/error.rs index 7b25e7e3..2ba0fe30 100644 --- a/src/error.rs +++ b/src/error.rs @@ -12,6 +12,7 @@ use web::extractors::RequestErrorLocation; /// Legacy Sync 1.1 error codes, which Sync 1.5 also returns by replacing the descriptive JSON /// information and replacing it with one of these error codes. +#[allow(dead_code)] #[derive(Serialize)] enum WeaveError { /// Unknown error diff --git a/src/web/error.rs b/src/web/error.rs index 8637aacf..3982cc88 100644 --- a/src/web/error.rs +++ b/src/web/error.rs @@ -106,10 +106,13 @@ impl From> for ValidationError { fn from(inner: Context) -> Self { let status = match inner.get_context() { ValidationErrorKind::FromDetails(ref _description, ref location, Some(ref name)) - if *location == RequestErrorLocation::Header - && name.eq_ignore_ascii_case("Content-Type") => + if *location == RequestErrorLocation::Header => { - StatusCode::UNSUPPORTED_MEDIA_TYPE + match name.to_ascii_lowercase().as_str() { + "accept" => StatusCode::NOT_ACCEPTABLE, + "content-type" => StatusCode::UNSUPPORTED_MEDIA_TYPE, + _ => StatusCode::BAD_REQUEST, + } } _ => StatusCode::BAD_REQUEST, }; diff --git a/src/web/extractors.rs b/src/web/extractors.rs index dfec9f82..bf1daf97 100644 --- a/src/web/extractors.rs +++ b/src/web/extractors.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; use std::str::FromStr; -use actix_web::http::header::{HeaderValue, CONTENT_TYPE}; +use actix_web::http::header::{HeaderValue, ACCEPT, CONTENT_TYPE}; use actix_web::{ dev::{JsonConfig, PayloadConfig}, error::ErrorInternalServerError, @@ -391,6 +391,13 @@ impl FromRequest for MetaRequest { } } +/// Desired reply format for a Collection Get request +#[derive(Copy, Clone, Debug)] +pub enum ReplyFormat { + Json, + Newlines, +} + /// Collection Request Delete/Get extractor /// /// Extracts/validates information needed for collection delete/get requests. @@ -399,6 +406,7 @@ pub struct CollectionRequest { pub db: Box, pub user_id: HawkIdentifier, pub query: BsoQueryParams, + pub reply: ReplyFormat, } impl FromRequest for CollectionRequest { @@ -410,12 +418,25 @@ impl FromRequest for CollectionRequest { let db = >::from_request(req, settings)?; let query = BsoQueryParams::from_request(req, settings)?; let collection = CollectionParam::from_request(req, settings)?.collection; + let reply = match req.headers().get(ACCEPT) { + Some(v) if v.as_bytes() == b"application/newlines" => ReplyFormat::Newlines, + Some(v) if v.as_bytes() == b"application/json" => ReplyFormat::Json, + Some(_) => { + return Err(ValidationErrorKind::FromDetails( + "Invalid accept".to_string(), + RequestErrorLocation::Header, + Some("accept".to_string()), + ).into()); + } + None => ReplyFormat::Json, + }; Ok(CollectionRequest { collection, db, user_id, query, + reply, }) } } diff --git a/src/web/handlers.rs b/src/web/handlers.rs index 4e20be0e..fd0b9ea5 100644 --- a/src/web/handlers.rs +++ b/src/web/handlers.rs @@ -10,7 +10,7 @@ use error::ApiError; use server::ServerState; use web::extractors::{ BsoPutRequest, BsoRequest, CollectionPostRequest, CollectionRequest, HawkIdentifier, - MetaRequest, + MetaRequest, ReplyFormat, }; pub const ONE_KB: f64 = 1024.0; @@ -128,6 +128,7 @@ where F: Future, Error = ApiError> + 'static, T: Serialize + Default + 'static, { + let reply_format = coll.reply; Box::new( fut.or_else(move |e| { if e.is_colllection_not_found() { @@ -143,13 +144,29 @@ where .extract_resource(coll.user_id, Some(coll.collection), None) .map_err(From::from) .map(move |ts| (result, ts)) - }).map(|(result, ts)| { - HttpResponse::build(StatusCode::OK) + }).map(move |(result, ts)| { + let mut builder = HttpResponse::build(StatusCode::OK); + let resp = builder .header("X-Last-Modified", ts.as_header()) .header("X-Weave-Records", result.items.len().to_string()) .if_some(result.offset, |offset, resp| { resp.header("X-Weave-Next-Offset", offset.to_string()); - }).json(result.items) + }); + match reply_format { + ReplyFormat::Json => resp.json(result.items), + ReplyFormat::Newlines => { + let items: String = result + .items + .into_iter() + .map(|v| serde_json::to_string(&v).unwrap_or("".to_string())) + .filter(|v| !v.is_empty()) + .map(|v| v.replace("\n", "\\u000a") + "\n") + .collect(); + resp.header("Content-Type", "application/newlines") + .header("Content-Length", format!("{}", items.len())) + .body(items) + } + } }), ) }