Skip to content

Commit 261b455

Browse files
authored
Merge pull request #141 from xsnippet/plain-text-get-snippet
Add a GET /snippet/:id that allows to retrieve the raw content of a snippet as plain text
2 parents 627a3a0 + de63750 commit 261b455

File tree

7 files changed

+80
-9
lines changed

7 files changed

+80
-9
lines changed

src/application.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ pub fn create_app() -> Result<rocket::Rocket, Box<dyn Error>> {
7070
routes::snippets::create_snippet,
7171
routes::snippets::list_snippets,
7272
routes::snippets::get_snippet,
73+
routes::snippets::get_raw_snippet,
7374
routes::syntaxes::get_syntaxes,
7475
routes::snippets::import_snippet,
7576
];

src/errors.rs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
use std::convert::From;
2-
31
use rocket::http;
42
use rocket::request::Request;
53
use rocket::response::{self, Responder, Response};
4+
use rocket::Outcome;
65
use serde::{ser::SerializeStruct, Serialize, Serializer};
76

87
use crate::storage::StorageError;
9-
use crate::web::Output;
8+
use crate::web::{NegotiatedContentType, Output};
109

1110
/// All possible unsuccessful outcomes of an API request.
1211
///
@@ -86,12 +85,22 @@ impl<'r> Responder<'r> for ApiError {
8685
.status(http::Status::InternalServerError)
8786
.ok()
8887
} else {
89-
// otherwise, present the error in the requested data format
9088
let http_status = self.status();
9189
debug!("ApiError: {:?}", self);
92-
Response::build_from(Output(self).respond_to(request)?)
90+
91+
match request.guard::<NegotiatedContentType>() {
92+
// otherwise, present the error in the requested data format if content negotiation
93+
// has succeeded
94+
Outcome::Success(_) => Response::build_from(Output(self).respond_to(request)?)
95+
.status(http_status)
96+
.ok(),
97+
// or as plain text if content negotiation has failed
98+
_ => Response::build_from(
99+
response::content::Plain(self.reason().to_string()).respond_to(request)?,
100+
)
93101
.status(http_status)
94-
.ok()
102+
.ok(),
103+
}
95104
}
96105
}
97106
}

src/routes/snippets.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ use crate::application::Config;
1111
use crate::errors::ApiError;
1212
use crate::storage::{Changeset, DateTime, Direction, ListSnippetsQuery, Snippet, Storage};
1313
use crate::web::{
14-
BearerAuth, Input, NegotiatedContentType, Output, PaginationLimit, WithHttpHeaders,
14+
BearerAuth, DoNotAcceptAny, Input, NegotiatedContentType, Output, PaginationLimit,
15+
WithHttpHeaders,
1516
};
1617

1718
fn create_snippet_impl(
@@ -260,7 +261,27 @@ pub fn import_snippet(
260261
create_snippet_impl(&**storage, &snippet, base_path)
261262
}
262263

263-
#[get("/snippets/<id>")]
264+
#[get("/snippets/<id>", format = "text/plain", rank = 1)]
265+
pub fn get_raw_snippet(
266+
storage: State<Box<dyn Storage>>,
267+
id: String,
268+
_user: BearerAuth,
269+
// W/o this, a request specifying any media type (i.e. */*), would be matched by this route,
270+
// which is not what we want. We can't add the desired format to the route below, because it's
271+
// supposed to perform content negotiation and return an ApiError when a user requests an
272+
// unsupported format.
273+
_not_any: DoNotAcceptAny,
274+
) -> Result<String, ApiError> {
275+
Ok(storage
276+
.get(&id)?
277+
.changesets
278+
.into_iter()
279+
.last()
280+
.map(|c| c.content)
281+
.unwrap_or_default())
282+
}
283+
284+
#[get("/snippets/<id>", rank = 2)]
264285
pub fn get_snippet(
265286
storage: State<Box<dyn Storage>>,
266287
id: String,

src/web.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ mod tracing;
44

55
pub use crate::web::auth::{AuthValidator, BearerAuth, JwtValidator, User};
66
pub use crate::web::content::{
7-
Input, NegotiatedContentType, Output, PaginationLimit, WithHttpHeaders,
7+
DoNotAcceptAny, Input, NegotiatedContentType, Output, PaginationLimit, WithHttpHeaders,
88
};
99
pub use crate::web::tracing::{RequestId, RequestIdHeader, RequestSpan};

src/web/content.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,3 +232,20 @@ impl<'a, 'r> FromRequest<'a, 'r> for NegotiatedContentType {
232232
}
233233
}
234234
}
235+
236+
/// A request guard that only accepts request that specify a specific media type
237+
/// via the Accept header.
238+
pub struct DoNotAcceptAny;
239+
240+
impl<'a, 'r> FromRequest<'a, 'r> for DoNotAcceptAny {
241+
type Error = ApiError;
242+
243+
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
244+
match request.accept() {
245+
// Accept is passed and it's not */*
246+
Some(accept) if !accept.preferred().is_any() => Success(DoNotAcceptAny),
247+
// Accept is not passed, or it's not specific (i.e. */*)
248+
_ => Forward(()),
249+
}
250+
}
251+
}

tests/gabbits/get-snippet-errors.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,15 @@ tests:
3535
status: 404
3636
response_headers:
3737
x-request-id: *request_id_regex
38+
39+
- name: retrieve the raw content of a snippet (not found)
40+
GET: /v1/snippets/foobar
41+
request_headers:
42+
accept: text/plain
43+
response_headers:
44+
content-type: text/plain; charset=utf-8
45+
response_strings:
46+
- Snippet with id `foobar` is not found
47+
status: 404
48+
response_headers:
49+
x-request-id: *request_id_regex

tests/gabbits/get-snippet.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,17 @@ tests:
8080
$.`len`: 7
8181
status: 200
8282

83+
- name: retrieve the raw content of a snippet
84+
GET: /v1/snippets/$HISTORY['create a new snippet'].$RESPONSE['id']
85+
request_headers:
86+
accept: "text/plain"
87+
response_headers:
88+
content-type: text/plain; charset=utf-8
89+
x-request-id: *request_id_regex
90+
response_strings:
91+
- print('Hello, World!')
92+
status: 200
93+
8394
- name: retrieve previously created snippet by ID (multiple accept types)
8495
GET: /v1/snippets/$HISTORY['create a new snippet'].$RESPONSE['id']
8596
request_headers:

0 commit comments

Comments
 (0)