diff --git a/poem-openapi-derive/Cargo.toml b/poem-openapi-derive/Cargo.toml index c4beb0d4e84..56c84307c80 100644 --- a/poem-openapi-derive/Cargo.toml +++ b/poem-openapi-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-openapi-derive" -version = "2.0.22" +version = "2.0.23" authors = ["sunli "] edition = "2021" description = "Macros for poem-openapi" diff --git a/poem-openapi/CHANGELOG.md b/poem-openapi/CHANGELOG.md index 3d04d742b2d..2bee5de45ee 100644 --- a/poem-openapi/CHANGELOG.md +++ b/poem-openapi/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# [2.0.23] 2022-01-13 + +- Add the missing feature `openapi-explorer` in `ui` mod [#480](https://github.com/poem-web/poem/pull/480) +- Add yaml support [#476](https://github.com/poem-web/poem/pull/476) +- Remove `poem_openapi::response::StaticFileResponse` and implement `ApiResponse trait` for `poem::web::StaticFileResponse` + # [2.0.22] 2023-01-11 - Add support for OpenAPI Explorer [#440](https://github.com/poem-web/poem/pull/440) diff --git a/poem-openapi/Cargo.toml b/poem-openapi/Cargo.toml index cef782deab1..fce5e5b0bc0 100644 --- a/poem-openapi/Cargo.toml +++ b/poem-openapi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-openapi" -version = "2.0.22" +version = "2.0.23" authors = ["sunli "] edition = "2021" description = "OpenAPI support for Poem." @@ -23,14 +23,14 @@ static-files = ["poem/static-files"] websocket = ["poem/websocket"] [dependencies] -poem-openapi-derive = { path = "../poem-openapi-derive", version = "2.0.22" } +poem-openapi-derive = { path = "../poem-openapi-derive", version = "2.0.23" } poem = { path = "../poem", version = "1.3.51", features = [ "multipart", "tempfile", "cookie", "sse", "xml", - "yaml" + "yaml", ] } tokio = { version = "1.17.0", features = ["fs"] } diff --git a/poem-openapi/src/lib.rs b/poem-openapi/src/lib.rs index f995381e82e..d1ea36b6d9d 100644 --- a/poem-openapi/src/lib.rs +++ b/poem-openapi/src/lib.rs @@ -124,7 +124,7 @@ pub mod param; pub mod payload; #[doc(hidden)] pub mod registry; -pub mod response; +mod response; pub mod types; #[doc(hidden)] pub mod validation; diff --git a/poem-openapi/src/response/mod.rs b/poem-openapi/src/response/mod.rs index c86685a4a68..fda1e07bae8 100644 --- a/poem-openapi/src/response/mod.rs +++ b/poem-openapi/src/response/mod.rs @@ -2,6 +2,3 @@ #[cfg(feature = "static-files")] mod static_file; - -#[cfg(feature = "static-files")] -pub use static_file::StaticFileResponse; diff --git a/poem-openapi/src/response/static_file.rs b/poem-openapi/src/response/static_file.rs index 8159a36ab11..b9c7a8d4f14 100644 --- a/poem-openapi/src/response/static_file.rs +++ b/poem-openapi/src/response/static_file.rs @@ -1,112 +1,85 @@ -use poem::{error::StaticFileError, Body}; +use poem::{web::StaticFileResponse, Body}; use crate::{ - payload::{Binary, PlainText}, + payload::{Binary, Payload}, + registry::{MetaHeader, MetaMediaType, MetaResponse, MetaResponses, Registry}, + types::Type, ApiResponse, }; -/// A static file response. -#[cfg_attr(docsrs, doc(cfg(feature = "static-files")))] -#[derive(ApiResponse)] -#[oai(internal)] -pub enum StaticFileResponse { - /// Ok - #[oai(status = 200)] - Ok( - Binary, - /// The ETag (or entity tag) HTTP response header is an identifier for a - /// specific version of a resource. It lets caches be more efficient and - /// save bandwidth, as a web server does not need to resend a full - /// response if the content was not changed. Additionally, etags help to - /// prevent simultaneous updates of a resource from overwriting each - /// other ("mid-air collisions"). - /// - /// Reference: - #[oai(header = "etag")] - Option, - /// The Last-Modified response HTTP header contains a date and time when - /// the origin server believes the resource was last modified. It is - /// used as a validator to determine if the resource is the same as the - /// previously stored one. Less accurate than an ETag header, it is a - /// fallback mechanism. Conditional requests containing - /// If-Modified-Since or If-Unmodified-Since headers make use of this - /// field. - /// - /// Reference: - #[oai(header = "last-modified")] - Option, - /// The Content-Type representation header is used to indicate the - /// original media type of the resource (prior to any content encoding - /// applied for sending). - /// - /// Reference: - #[oai(header = "content-type")] - Option, - ), - /// Not modified - /// - /// Reference: - #[oai(status = 304)] - NotModified, - /// Bad request - /// - /// Reference: - #[oai(status = 400)] - BadRequest, - /// Resource was not found - #[oai(status = 404)] - NotFound, - /// Precondition failed - /// - /// Reference: - #[oai(status = 412)] - PreconditionFailed, - /// Range not satisfiable - /// - /// Reference: - #[oai(status = 416)] - RangeNotSatisfiable( - /// The Content-Range response HTTP header indicates where in a full - /// body message a partial message belongs. - /// - /// Reference: - #[oai(header = "content-range")] - String, - ), - /// Internal server error - #[oai(status = 500)] - InternalServerError(PlainText), -} +const ETAG_DESCRIPTION: &str = r#"The ETag (or entity tag) HTTP response header is an identifier for a specific version of a resource. It lets caches be more efficient and save bandwidth, as a web server does not need to resend a full response if the content was not changed. Additionally, etags help to prevent simultaneous updates of a resource from overwriting each other ("mid-air collisions")."#; +const LAST_MODIFIED_DESCRIPTION: &str = r#"The Last-Modified response HTTP header contains a date and time when the origin server believes the resource was last modified. It is used as a validator to determine if the resource is the same as the previously stored one. Less accurate than an ETag header, it is a fallback mechanism. Conditional requests containing If-Modified-Since or If-Unmodified-Since headers make use of this field."#; +const CONTENT_TYPE_DESCRIPTION: &str = r#"The Content-Type representation header is used to indicate the original media type of the resource (prior to any content encoding applied for sending)."#; -impl StaticFileResponse { - /// Create a static file response. - pub fn new(res: Result) -> Self { - res.into() - } -} - -impl From> for StaticFileResponse { - fn from(res: Result) -> Self { - match res { - Ok(poem::web::StaticFileResponse::Ok { - body, - etag, - last_modified, - content_type, - .. - }) => StaticFileResponse::Ok(Binary(body), etag, last_modified, content_type), - Ok(poem::web::StaticFileResponse::NotModified) => StaticFileResponse::NotModified, - Err( - StaticFileError::MethodNotAllowed(_) - | StaticFileError::NotFound - | StaticFileError::InvalidPath - | StaticFileError::Forbidden(_), - ) => StaticFileResponse::NotFound, - Err(StaticFileError::PreconditionFailed) => StaticFileResponse::PreconditionFailed, - Err(StaticFileError::RangeNotSatisfiable { size }) => { - StaticFileResponse::RangeNotSatisfiable(format!("*/{}", size)) - } - Err(StaticFileError::Io(_)) => StaticFileResponse::BadRequest, +impl ApiResponse for StaticFileResponse { + fn meta() -> MetaResponses { + MetaResponses { + responses: vec![ + MetaResponse { + description: "", + status: Some(200), + content: vec![MetaMediaType { + content_type: Binary::::CONTENT_TYPE, + schema: Binary::::schema_ref(), + }], + headers: vec![MetaHeader { + name: "etag".to_string(), + description: Some(ETAG_DESCRIPTION.to_string()), + required: false, + deprecated: false, + schema: String::schema_ref(), + }, MetaHeader { + name: "last-modified".to_string(), + description: Some(LAST_MODIFIED_DESCRIPTION.to_string()), + required: false, + deprecated: false, + schema: String::schema_ref(), + }, MetaHeader { + name: "content-type".to_string(), + description: Some(CONTENT_TYPE_DESCRIPTION.to_string()), + required: false, + deprecated: false, + schema: String::schema_ref(), + }], + }, + MetaResponse { + description: "Not modified", + status: Some(304), + content: vec![], + headers: vec![], + }, + MetaResponse { + description: "Bad request", + status: Some(400), + content: vec![], + headers: vec![], + }, + MetaResponse { + description: "Resource was not found", + status: Some(404), + content: vec![], + headers: vec![], + }, + MetaResponse { + description: "Precondition failed", + status: Some(412), + content: vec![], + headers: vec![], + }, + MetaResponse { + description: "The Content-Range response HTTP header indicates where in a full body message a partial message belongs.", + status: Some(416), + content: vec![], + headers: vec![], + }, MetaResponse { + description: "Internal server error", + status: Some(500), + content: vec![], + headers: vec![], + }, + ], } } + + fn register(_registry: &mut Registry) {} } diff --git a/poem/src/web/static_file.rs b/poem/src/web/static_file.rs index 72a6a4b36ea..7b121d9f634 100644 --- a/poem/src/web/static_file.rs +++ b/poem/src/web/static_file.rs @@ -7,6 +7,7 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; +use bytes::Bytes; use headers::{ ContentRange, ETag, HeaderMapExt, IfMatch, IfModifiedSince, IfNoneMatch, IfUnmodifiedSince, Range, @@ -42,6 +43,16 @@ pub enum StaticFileResponse { NotModified, } +impl StaticFileResponse { + /// Set the content type + pub fn with_content_type(mut self, ct: impl Into) -> Self { + if let StaticFileResponse::Ok { content_type, .. } = &mut self { + *content_type = Some(ct.into()); + } + self + } +} + impl IntoResponse for StaticFileResponse { fn into_response(self) -> Response { match self { @@ -104,6 +115,59 @@ impl<'a> FromRequest<'a> for StaticFileRequest { } impl StaticFileRequest { + /// Create static file response. + /// + /// `prefer_utf8` - Specifies whether text responses should signal a UTF-8 + /// encoding. + pub fn create_response_from_data( + self, + data: impl AsRef<[u8]>, + ) -> Result { + let data = data.as_ref(); + + // content length + let mut content_length = data.len() as u64; + let mut content_range = None; + + let body = if let Some((start, end)) = self.range.and_then(|range| range.iter().next()) { + let start = match start { + Bound::Included(n) => n, + Bound::Excluded(n) => n + 1, + Bound::Unbounded => 0, + }; + let end = match end { + Bound::Included(n) => n + 1, + Bound::Excluded(n) => n, + Bound::Unbounded => content_length, + }; + if end < start || end > content_length { + return Err(StaticFileError::RangeNotSatisfiable { + size: content_length, + }); + } + + if start != 0 || end != content_length { + content_range = Some((start..end, content_length)); + } + + content_length = end - start; + Body::from_bytes(Bytes::copy_from_slice( + &data[start as usize..(start + content_length) as usize], + )) + } else { + Body::from_bytes(Bytes::copy_from_slice(data)) + }; + + Ok(StaticFileResponse::Ok { + body, + content_length, + content_type: None, + etag: None, + last_modified: None, + content_range, + }) + } + /// Create static file response. /// /// `prefer_utf8` - Specifies whether text responses should signal a UTF-8