Repository: wafflespeanut/paperclip
Pull request #243 (2020-10-06 10:26:28)

Add types for encoding successful response codes
Add types to encode successful response codes in actix-web:
* `NoContent`
* `AcceptedJson`
* `CreatedJson`

Allows status codes to be used both by paperclip and actix-web, eg.
```rust
use paperclip::actix::web::Json;
use paperclip::actix::{CreatedJson, AcceptedJson, NoContent};

// 201 Created 
#[api_v2_operation]
async fn adopt_pet(body: Json<Pet>) -> Result<CreatedJson<Pet>, ()> {
    let pet: Pet = body.into_inner();
    // bring the pet home
    Ok(CreatedJson(pet))
}
// 204 No Content
#[api_v2_operation]
async fn acknowledge_pet(body: Json<Pet>) -> NoContent {
    NoContent
}
```

I was not really sure where to put this, I put it in `paperclip_core::v2::actix` because it fit there the easiest.

#62 #241 



platy: I decided to combine the status code with the Json wrapper because it makes the usage shorter, but maybe it makes sense to separate them and leave it up to the user to define types and functions which make the usage better.
wafflespeanut: This would certainly be the way in the previous version of papercliop, but now we're generating the code for each handler, so we can solve this in an easier way through the macro attribute :smile: 
```
#[api_v2_operation(status(code = 202, description = "I accept"))]  // we can support multiple statuses too
fn my_handler () -> Foo { /* */ }
```
What do you think?
platy: I really appreciate the macro attribute for being able to describe what the status code means for that particular handler, but I appreciate the return type for joining the behaviour with the spec and meaning that the spec can't be wrong.

Patch:
diff --git a/book/actix-plugin.md b/book/actix-plugin.md
index eff5a75f..dae24148 100644
--- a/book/actix-plugin.md
+++ b/book/actix-plugin.md
@@ -203,7 +203,29 @@ This can be overridden by explicitly specifying `summary` and `description` in t
 async fn my_handler() -> Json<Foo> { /* */ }
 ```
 
-#### Manually defining additional response codes
+#### Using other (non-200) response codes
+
+Paperclip finds out the schema of your api using macros which read the types of the handlers and parameter structs at compile time, so in order for paperclip to know what response code the api sends, it needs type information about it, it is not sufficient to store the code in the response. There are newtypes encoding this information for the most common 2xx codes (OK, created, accepted) for Json responses and no content.
+
+```rust
+use paperclip::actix::web::Json;
+use paperclip::actix::{CreatedJson, AcceptedJson, NoContent};
+
+// 201 Created 
+#[api_v2_operation]
+async fn adopt_pet(body: Json<Pet>) -> Result<CreatedJson<Pet>, ()> {
+    let pet: Pet = body.into_inner();
+    // bring the pet home
+    Ok(CreatedJson(pet))
+}
+// 204 No Content
+#[api_v2_operation]
+async fn acknowledge_pet(body: Json<Pet>) -> NoContent {
+    NoContent
+}
+```
+
+#### Manually defining error response codes
 
 There is a macro `api_v2_errors` which helps to manually add error (non-2xx) response codes.
 
diff --git a/core/src/v2/actix.rs b/core/src/v2/actix.rs
index 580f1639..f9d60440 100644
--- a/core/src/v2/actix.rs
+++ b/core/src/v2/actix.rs
@@ -5,16 +5,20 @@ use super::models::{
 #[cfg(feature = "actix-multipart")]
 use super::schema::TypedData;
 use super::schema::{Apiv2Errors, Apiv2Operation, Apiv2Schema};
+use crate::util::{ready, Ready};
 use actix_web::{
+    http::StatusCode,
     web::{Bytes, Data, Form, Json, Path, Payload, Query},
-    HttpRequest, HttpResponse, Responder,
+    Error, HttpRequest, HttpResponse, Responder,
 };
 use pin_project::pin_project;
 
+use serde::Serialize;
 #[cfg(feature = "serde_qs")]
 use serde_qs::actix::QsQuery;
 
 use std::collections::BTreeMap;
+use std::fmt;
 use std::future::Future;
 use std::pin::Pin;
 use std::task::{Context, Poll};
@@ -521,3 +525,120 @@ where
         new.update_definitions(name, map);
     }
 }
+
+macro_rules! json_with_status {
+    ($name:ident => $status:expr) => {
+        pub struct $name<T: Serialize + Apiv2Schema>(pub T);
+
+        impl<T> fmt::Debug for $name<T>
+        where
+            T: fmt::Debug + Serialize + Apiv2Schema,
+        {
+            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+                let status: StatusCode = $status;
+                let status_str = status.canonical_reason().unwrap_or(status.as_str());
+                write!(f, "{} Json: {:?}", status_str, self.0)
+            }
+        }
+
+        impl<T> fmt::Display for $name<T>
+        where
+            T: fmt::Display + Serialize + Apiv2Schema,
+        {
+            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+                fmt::Display::fmt(&self.0, f)
+            }
+        }
+
+        impl<T> Responder for $name<T>
+        where
+            T: Serialize + Apiv2Schema,
+        {
+            type Error = Error;
+            type Future = Ready<Result<HttpResponse, Error>>;
+
+            fn respond_to(self, _: &HttpRequest) -> Self::Future {
+                let status: StatusCode = $status;
+                let body = match serde_json::to_string(&self.0) {
+                    Ok(body) => body,
+                    Err(e) => return ready(Err(e.into())),
+                };
+
+                ready(Ok(HttpResponse::build(status)
+                    .content_type("application/json")
+                    .body(body)))
+            }
+        }
+
+        impl<T> Apiv2Schema for $name<T>
+        where
+            T: Serialize + Apiv2Schema,
+        {
+            const NAME: Option<&'static str> = T::NAME;
+
+            fn raw_schema() -> DefaultSchemaRaw {
+                T::raw_schema()
+            }
+        }
+
+        impl<T> OperationModifier for $name<T>
+        where
+            T: Serialize + Apiv2Schema,
+        {
+            fn update_response(op: &mut DefaultOperationRaw) {
+                let status: StatusCode = $status;
+                op.responses.insert(
+                    status.as_str().into(),
+                    Either::Right(Response {
+                        description: status.canonical_reason().map(ToString::to_string),
+                        schema: Some({
+                            let mut def = T::schema_with_ref();
+                            def.retain_ref();
+                            def
+                        }),
+                        ..Default::default()
+                    }),
+                );
+            }
+        }
+    };
+}
+
+json_with_status!(CreatedJson => StatusCode::CREATED);
+json_with_status!(AcceptedJson => StatusCode::ACCEPTED);
+
+#[derive(Debug)]
+pub struct NoContent;
+
+impl fmt::Display for NoContent {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.write_str("No Content")
+    }
+}
+
+impl Responder for NoContent {
+    type Error = Error;
+    type Future = Ready<Result<HttpResponse, Error>>;
+
+    fn respond_to(self, _: &HttpRequest) -> Self::Future {
+        ready(Ok(HttpResponse::build(StatusCode::NO_CONTENT)
+            .content_type("application/json")
+            .finish()))
+    }
+}
+
+impl Apiv2Schema for NoContent {}
+
+impl OperationModifier for NoContent {
+    fn update_response(op: &mut DefaultOperationRaw) {
+        let status = StatusCode::NO_CONTENT;
+        op.responses.insert(
+            status.as_str().into(),
+            Either::Right(Response {
+                description: status.canonical_reason().map(ToString::to_string),
+                schema: None,
+                ..Default::default()
+            }),
+        );
+    }
+}
diff --git a/core/src/v2/mod.rs b/core/src/v2/mod.rs
index 8126e441..423d5435 100644
--- a/core/src/v2/mod.rs
+++ b/core/src/v2/mod.rs
@@ -10,7 +10,9 @@ mod resolver;
 pub mod schema;
 
 #[cfg(feature = "actix-base")]
-pub use self::actix::{OperationModifier, ResponderWrapper, ResponseWrapper};
+pub use self::actix::{
+    AcceptedJson, CreatedJson, NoContent, OperationModifier, ResponderWrapper, ResponseWrapper,
+};
 
 pub use self::models::{DefaultSchema, ResolvableApi};
 pub use self::schema::Schema;
diff --git a/src/lib.rs b/src/lib.rs
index 4878f39d..91f0210c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -23,5 +23,6 @@ pub mod actix {
 
     pub use paperclip_actix::{api_v2_errors, api_v2_operation, Apiv2Schema, Apiv2Security};
     pub use paperclip_actix::{web, App, Mountable, OpenApiExt};
+    pub use paperclip_core::v2::{AcceptedJson, CreatedJson, NoContent};
     pub use paperclip_core::v2::{OperationModifier, ResponderWrapper, ResponseWrapper};
 }


Test patch:
diff --git a/tests/test_app.rs b/tests/test_app.rs
index cd494202..103864b2 100644
--- a/tests/test_app.rs
+++ b/tests/test_app.rs
@@ -10,7 +10,8 @@ use actix_web::{App, Error, FromRequest, HttpRequest, HttpServer, Responder};
 use futures::future::{ok as fut_ok, ready, Future, Ready};
 use once_cell::sync::Lazy;
 use paperclip::actix::{
-    api_v2_errors, api_v2_operation, web, Apiv2Schema, Apiv2Security, OpenApiExt,
+    api_v2_errors, api_v2_operation, web, Apiv2Schema, Apiv2Security, CreatedJson, NoContent,
+    OpenApiExt,
 };
 use parking_lot::Mutex;
 
@@ -100,10 +101,23 @@ fn test_simple_app() {
         fut_ok(unimplemented!())
     }
 
+    #[api_v2_operation]
+    async fn adopt_pet() -> Result<CreatedJson<Pet>, ()> {
+        let pet: Pet = Pet::default();
+        Ok(CreatedJson(pet))
+    }
+
+    #[api_v2_operation]
+    async fn nothing() -> NoContent {
+        NoContent
+    }
+
     fn config(cfg: &mut web::ServiceConfig) {
         cfg.service(web::resource("/echo").route(web::post().to(echo_pet)))
             .service(web::resource("/async_echo").route(web::post().to(echo_pet_async)))
             .service(web::resource("/async_echo_2").route(web::post().to(echo_pet_async_2)))
+            .service(web::resource("/adopt").route(web::post().to(adopt_pet)))
+            .service(web::resource("/nothing").route(web::get().to(nothing)))
             .service(web::resource("/random").to(some_pet));
     }
 
@@ -288,7 +302,28 @@ fn test_simple_app() {
                           }
                         }
                       }
-                    }
+                    },
+                    "/api/adopt": {
+                      "post": {
+                        "responses": {
+                          "201": {
+                            "description": "Created",
+                            "schema": {
+                              "$ref": "#/definitions/Pet"
+                            }
+                          }
+                        }
+                      }
+                    },
+                    "/api/nothing": {
+                      "get": {
+                        "responses": {
+                          "204": {
+                            "description": "No Content"
+                          }
+                        }
+                      }
+                    },
                   },
                   "swagger": "2.0"
                 }),