Skip to content

Commit

Permalink
Merge pull request #122 from xsnippet/output-alternative
Browse files Browse the repository at this point in the history
Implement basic content type negotiation (alternative)
  • Loading branch information
malor committed Feb 20, 2021
2 parents 31382f3 + 622d48e commit 8f8bd31
Show file tree
Hide file tree
Showing 12 changed files with 456 additions and 121 deletions.
32 changes: 17 additions & 15 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,37 @@
use std::convert::From;
use std::io::Cursor;

use rocket::http;
use rocket::request::Request;
use rocket::response::{self, Responder, Response};
use serde::{ser::SerializeStruct, Serialize, Serializer};

use crate::storage::StorageError;
use crate::web::Output;

/// All possible unsuccessful outcomes of an API request.
///
/// Allows to handle application errors in the unified manner:
///
/// 1) all errors are serialized to JSON messages like {"message": "..."};
/// 1) all errors are serialized to the requested content type
/// the HTTP status code is set accordingly
///
/// 2) implements conversions from internal errors types (e.g. the errors
/// returned by the Storage trait)
#[derive(Debug)]
pub enum ApiError {
BadRequest(String),
NotFound(String),
InternalError(String),
UnsupportedMediaType(String),
BadRequest(String), // ==> HTTP 400 Bad Request
NotFound(String), // ==> HTTP 404 Not Found
NotAcceptable(&'static str), // ==> HTTP 406 Not Acceptable
UnsupportedMediaType(&'static str), // ==> HTTP 415 Unsupported Media Type
InternalError(String), // ==> HTTP 500 Internal Server Error
}

impl ApiError {
/// Reason why the request failed.
pub fn reason(&self) -> &str {
match self {
ApiError::BadRequest(msg) => &msg,
ApiError::NotAcceptable(msg) => &msg,
ApiError::NotFound(msg) => &msg,
ApiError::InternalError(msg) => &msg,
ApiError::UnsupportedMediaType(msg) => &msg,
Expand All @@ -40,6 +42,7 @@ impl ApiError {
pub fn status(&self) -> http::Status {
match self {
ApiError::BadRequest(_) => http::Status::BadRequest,
ApiError::NotAcceptable(_) => http::Status::NotAcceptable,
ApiError::NotFound(_) => http::Status::NotFound,
ApiError::UnsupportedMediaType(_) => http::Status::UnsupportedMediaType,
ApiError::InternalError(_) => http::Status::InternalServerError,
Expand Down Expand Up @@ -68,20 +71,19 @@ impl Serialize for ApiError {
}

impl<'r> Responder<'r> for ApiError {
fn respond_to(self, _request: &Request) -> response::Result<'r> {
let mut response = Response::build();

fn respond_to(self, request: &Request) -> response::Result<'r> {
if let ApiError::InternalError(_) = self {
// do not give away any details for internal server errors
// TODO: integrate with Rocket contextual logging when 0.5 is released
eprintln!("Internal server error: {}", self.reason());
response.status(http::Status::InternalServerError).ok()
Response::build()
.status(http::Status::InternalServerError)
.ok()
} else {
// otherwise, present the error as JSON and set the status code accordingly
response
.status(self.status())
.header(http::ContentType::JSON)
.sized_body(Cursor::new(json!(self).to_string()))
// otherwise, present the error in the requested data format
let http_status = self.status();
Response::build_from(Output(self).respond_to(request)?)
.status(http_status)
.ok()
}
}
Expand Down
4 changes: 1 addition & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@
extern crate diesel;
#[macro_use]
extern crate rocket;
#[macro_use]
extern crate rocket_contrib;

mod application;
mod errors;
mod routes;
mod storage;
mod util;
mod web;

fn main() {
let app = match application::create_app() {
Expand Down
19 changes: 10 additions & 9 deletions src/routes/snippets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ use std::collections::BTreeSet;
use rocket::http::uri::Origin;
use rocket::response::status::Created;
use rocket::State;
use rocket_contrib::json::JsonValue;
use serde::Deserialize;

use crate::application::Config;
use crate::errors::ApiError;
use crate::storage::{Changeset, Snippet, Storage};
use crate::util::Input;
use crate::web::{Input, NegotiatedContentType, Output};

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
Expand Down Expand Up @@ -57,20 +56,22 @@ impl NewSnippet {

#[post("/snippets", data = "<body>")]
pub fn create_snippet(
origin: &Origin,
config: State<Config>,
storage: State<Box<dyn Storage>>,
origin: &Origin,
body: Result<Input<NewSnippet>, ApiError>,
) -> Result<Created<JsonValue>, ApiError> {
_content_type: NegotiatedContentType,
) -> Result<Created<Output<Snippet>>, ApiError> {
let new_snippet = storage.create(&body?.0.validate(config.syntaxes.as_ref())?)?;

let location = vec![origin.path().to_string(), new_snippet.id.to_string()].join("/");
let response = json!(new_snippet);

Ok(Created(location, Some(response)))
Ok(Created(location, Some(Output(new_snippet))))
}

#[get("/snippets/<id>")]
pub fn get_snippet(storage: State<Box<dyn Storage>>, id: String) -> Result<JsonValue, ApiError> {
Ok(json!(storage.get(&id)?))
pub fn get_snippet(
storage: State<Box<dyn Storage>>,
id: String,
) -> Result<Output<Snippet>, ApiError> {
Ok(Output(storage.get(&id)?))
}
9 changes: 6 additions & 3 deletions src/routes/syntaxes.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
use std::collections::BTreeSet;

use rocket::State;
use rocket_contrib::json::JsonValue;

use crate::application::Config;
use crate::errors::ApiError;
use crate::web::Output;

#[get("/syntaxes")]
pub fn get_syntaxes(config: State<Config>) -> JsonValue {
pub fn get_syntaxes(config: State<Config>) -> Result<Output<Option<BTreeSet<String>>>, ApiError> {
// This is a static route that simply returns a list of supported syntaxes.
// Normally XSnippet API clients must inspect these values and use them with
// submitted snippets.
json!(config.syntaxes)
Ok(Output(config.syntaxes.to_owned()))
}
88 changes: 0 additions & 88 deletions src/util.rs

This file was deleted.

3 changes: 3 additions & 0 deletions src/web.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod content;

pub use content::{Input, NegotiatedContentType, Output};

0 comments on commit 8f8bd31

Please sign in to comment.