diff --git a/core/codegen/src/attribute/route/mod.rs b/core/codegen/src/attribute/route/mod.rs index a24a2c4326..c9d7e3b065 100644 --- a/core/codegen/src/attribute/route/mod.rs +++ b/core/codegen/src/attribute/route/mod.rs @@ -296,6 +296,25 @@ fn sentinels_expr(route: &Route) -> TokenStream { quote!(::std::vec![#(#sentinel),*]) } +fn schemas_expr(route: &Route) -> TokenStream { + let ret_ty = match route.handler.sig.output { + syn::ReturnType::Default => None, + syn::ReturnType::Type(_, ref ty) => Some(ty.with_stripped_lifetimes()) + }; + + let eligible_types = route.guards() + .map(|guard| &guard.ty) + .chain(ret_ty.as_ref().into_iter()); + + let schema = eligible_types.map(|ty| { + define_spanned_export!(ty.span() => _doc); + + quote_spanned!(ty.span() => #_doc::resolve_doc!(#ty)) + }); + + quote!(::std::vec![#(#schema),*]) +} + fn codegen_route(route: Route) -> Result { use crate::exports::*; @@ -305,8 +324,9 @@ fn codegen_route(route: Route) -> Result { let query_guards = query_decls(&route); let data_guard = route.data_guard.as_ref().map(data_guard_decl); - // Extract the sentinels from the route. + // Extract the sentinels and schemas from the route. let sentinels = sentinels_expr(&route); + let schemas = schemas_expr(&route); // Gather info about the function. let (vis, handler_fn) = (&route.handler.vis, &route.handler); @@ -319,6 +339,9 @@ fn codegen_route(route: Route) -> Result { let rank = Optional(route.attr.rank); let format = Optional(route.attr.format.as_ref()); + // Get the doc comment + let docstring = &route.docstring; + Ok(quote! { #handler_fn @@ -353,6 +376,8 @@ fn codegen_route(route: Route) -> Result { format: #format, rank: #rank, sentinels: #sentinels, + schemas: #schemas, + docstring: #docstring, } } diff --git a/core/codegen/src/attribute/route/parse.rs b/core/codegen/src/attribute/route/parse.rs index 0c83c26170..33de6e0ef9 100644 --- a/core/codegen/src/attribute/route/parse.rs +++ b/core/codegen/src/attribute/route/parse.rs @@ -29,6 +29,8 @@ pub struct Route { pub handler: syn::ItemFn, /// The parsed arguments to the user's function. pub arguments: Arguments, + /// The doc comment describing this route + pub docstring: String, } type ArgumentMap = IndexMap; @@ -209,9 +211,11 @@ impl Route { }) .collect(); + let docstring = String::from_attrs("doc", &handler.attrs)?.join("\n"); + diags.head_err_or(Route { attr, path_params, query_params, data_guard, request_guards, - handler, arguments, + handler, arguments, docstring }) } } diff --git a/core/codegen/src/exports.rs b/core/codegen/src/exports.rs index 6f2ec05a3d..70705b8088 100644 --- a/core/codegen/src/exports.rs +++ b/core/codegen/src/exports.rs @@ -72,6 +72,7 @@ define_exported_paths! { _route => ::rocket::route, _catcher => ::rocket::catcher, _sentinel => ::rocket::sentinel, + _doc => ::rocket::doc, _log => ::rocket::log, _form => ::rocket::form::prelude, _http => ::rocket::http, diff --git a/core/lib/src/doc/has_schema.rs b/core/lib/src/doc/has_schema.rs new file mode 100644 index 0000000000..48f2bae3e5 --- /dev/null +++ b/core/lib/src/doc/has_schema.rs @@ -0,0 +1,318 @@ +pub enum SchemaKind { + Null, + Map, + List, + String, + Num, + Int, + Bool, + Set, +} + +pub struct TypeSchema { + pub description: Option, + pub example: Option, + pub name: String, + pub kind: SchemaKind, + +} + +pub trait HasSchema: Sized { + fn schema() -> TypeSchema; +} + +// impls for the entire serde data model: + +// 14 primitve types +impl HasSchema for i8 { + fn schema() -> TypeSchema { + TypeSchema { + description: None, + example: Some(1), + name: "signed 8-bits integer".to_string(), + kind: SchemaKind::Int, + } + } +} + +impl HasSchema for i16 { + fn schema() -> TypeSchema { + TypeSchema { + description: None, + example: Some(1), + name: "signed 16-bits integer".to_string(), + kind: SchemaKind::Int, + } + } +} + +impl HasSchema for i32 { + fn schema() -> TypeSchema { + TypeSchema { + description: None, + example: Some(1), + name: "signed 32-bits integer".to_string(), + kind: SchemaKind::Int, + } + } +} + +impl HasSchema for i64 { + fn schema() -> TypeSchema { + TypeSchema { + description: None, + example: Some(1), + name: "signed 64-bits integer".to_string(), + kind: SchemaKind::Int, + } + } +} + +impl HasSchema for i128 { + fn schema() -> TypeSchema { + TypeSchema { + description: None, + example: Some(1), + name: "signed 128-bits integer".to_string(), + kind: SchemaKind::Int, + } + } +} + +impl HasSchema for u8 { + fn schema() -> TypeSchema { + TypeSchema { + description: None, + example: Some(1), + name: "unsigned 8-bits integer".to_string(), + kind: SchemaKind::Int, + } + } +} + +impl HasSchema for u16 { + fn schema() -> TypeSchema { + TypeSchema { + description: None, + example: Some(1), + name: "unsigned 16-bits integer".to_string(), + kind: SchemaKind::Int, + } + } +} + +impl HasSchema for u32 { + fn schema() -> TypeSchema { + TypeSchema { + description: None, + example: Some(1), + name: "unsigned 32-bits integer".to_string(), + kind: SchemaKind::Int, + } + } +} + +impl HasSchema for u64 { + fn schema() -> TypeSchema { + TypeSchema { + description: None, + example: Some(1), + name: "unsigned 64-bits integer".to_string(), + kind: SchemaKind::Int, + } + } +} + +impl HasSchema for u128 { + fn schema() -> TypeSchema { + TypeSchema { + description: None, + example: Some(1), + name: "unsigned 128-bits integer".to_string(), + kind: SchemaKind::Int, + } + } +} + +impl HasSchema for f32 { + fn schema() -> TypeSchema { + TypeSchema { + description: None, + example: Some(1.0), + name: "32-bits floating point".to_string(), + kind: SchemaKind::Num, + } + } +} + +impl HasSchema for f64 { + fn schema() -> TypeSchema { + TypeSchema { + description: None, + example: Some(1.0), + name: "64-bits floating point".to_string(), + kind: SchemaKind::Num, + } + } +} + +impl HasSchema for bool { + fn schema() -> TypeSchema { + TypeSchema { + description: None, + example: Some(true), + name: "boolean".to_string(), + kind: SchemaKind::Bool, + } + } +} + +// string +impl<'a> HasSchema for &'a str { + fn schema() -> TypeSchema { + TypeSchema { + description: None, + example: Some("string"), + name: "signed 8-bits integer".to_string(), + kind: SchemaKind::String, + } + } +} + +impl<'a> HasSchema for String { + fn schema() -> TypeSchema { + TypeSchema { + description: None, + example: Some("string".to_string()), + name: "signed 8-bits integer".to_string(), + kind: SchemaKind::String, + } + } +} + +// byte array +impl<'a> HasSchema for &'a [u8] { + fn schema() -> TypeSchema { + TypeSchema { + description: None, + example: None, + name: "An array of bytes".to_string(), + kind: SchemaKind::List, + } + } +} + +// option +impl HasSchema for Option { + fn schema() -> TypeSchema { + let base_schema = T::schema(); + TypeSchema { + description: None, + example: Some(base_schema.example), + name: format!("Optional: {}", base_schema.name), + kind: base_schema.kind, + } + } +} + +// unit +impl HasSchema for () { + fn schema() -> TypeSchema { + TypeSchema { + description: None, + example: Some(()), + name: "Nothing".to_string(), + kind: SchemaKind::Null, + } + } +} + +// seq +impl HasSchema for [T; N] { + fn schema() -> TypeSchema { + let base_schema = T::schema(); + TypeSchema { + description: None, + example: None, // making an array example requires that T be Copy... + name: format!("Array of {} {}'s", N, base_schema.name), + kind: SchemaKind::List, + } + } +} + +impl HasSchema for Vec { + fn schema() -> TypeSchema { + let base_schema = T::schema(); + TypeSchema { + description: None, + example: None, // making an array example requires that T be Copy... + name: format!("Unsized array of {}'s", base_schema.name), + kind: SchemaKind::List, + } + } +} + +impl HasSchema for std::collections::HashSet { + fn schema() -> TypeSchema { + TypeSchema { + description: None, + example: None, // making an array example requires that T be Copy... + name: format!("Set of {}'s", T::schema().name), + kind: SchemaKind::Set, + } + } +} + +// tuple +impl HasSchema for (T1, ) { + fn schema() -> TypeSchema { + TypeSchema { + description: None, + example: None, // making an array example requires that T be Copy... + name: format!("Unary tuple of an {}", T1::schema().name), + kind: SchemaKind::Set, + } + } +} + +impl HasSchema for (T1, T2) { + fn schema() -> TypeSchema { + TypeSchema { + description: None, + example: None, // making an array example requires that T be Copy... + name: format!("Tuple of the form ({}, {})", T1::schema().name, T2::schema().name), + kind: SchemaKind::Set, + } + } +} + +// todo: extend with macros + +// map +impl HasSchema for std::collections::HashMap { + fn schema() -> TypeSchema { + TypeSchema { + description: None, + example: None, // making an array example requires that T be Copy... + name: format!("Map from {} to {}", K::schema().name, V::schema().name), + kind: SchemaKind::Map, + } + } +} + + + +// impl HasSchema for Box { +// fn schema() -> TypeSchema { +// let base_schema = T::schema(); +// TypeSchema { +// description: base_schema.description, +// example: base_schema.example.map(Box::new), +// name: base_schema.name, +// kind: base_schema.kind, +// } +// } +// } + + + diff --git a/core/lib/src/doc/mod.rs b/core/lib/src/doc/mod.rs new file mode 100644 index 0000000000..7aced5164a --- /dev/null +++ b/core/lib/src/doc/mod.rs @@ -0,0 +1,69 @@ +//! Traits and structs related to automagically generating documentation for your Rocket routes + +use std::{collections::HashMap, marker::PhantomData}; + +use rocket_http::ContentType; + +mod has_schema; + +#[derive(Default)] +pub struct Docs(HashMap); + +#[derive(Default)] +pub struct DocContent { + title: Option, + description: Option, + content_type: Option, +} + + +pub trait Documented { + fn docs() -> Docs; +} + +pub struct Schema { + pub type_name: &'static str, + pub documented: bool, + pub docs: Docs, +} + +#[doc(hidden)] +#[macro_export] +macro_rules! resolve_doc { + ($T:ty) => ({ + #[allow(unused_imports)] + use $crate::doc::resolution::{Resolve, Undocumented as _}; + + $crate::doc::Schema { + type_name: std::any::type_name::<$T>(), + documented: Resolve::<$T>::DOCUMENTED, + docs: Resolve::<$T>::docs(), + } + }) +} + +pub use resolve_doc; + +pub mod resolution { + use super::*; + + pub struct Resolve(PhantomData); + + pub trait Undocumented { + const DOCUMENTED: bool = false; + + fn docs() -> Docs { + Docs::default() + } + } + + impl Undocumented for T { } + + impl Resolve { + pub const DOCUMENTED: bool = true; + + pub fn docs() -> Docs { + T::docs() + } + } +} diff --git a/core/lib/src/lib.rs b/core/lib/src/lib.rs index cf64ecf8e3..c62f49aef9 100644 --- a/core/lib/src/lib.rs +++ b/core/lib/src/lib.rs @@ -124,6 +124,7 @@ pub mod fairing; pub mod error; pub mod catcher; pub mod route; +pub mod doc; // Reexport of HTTP everything. pub mod http { diff --git a/core/lib/src/route/route.rs b/core/lib/src/route/route.rs index 618d5d1b2a..a4c5680099 100644 --- a/core/lib/src/route/route.rs +++ b/core/lib/src/route/route.rs @@ -7,6 +7,7 @@ use yansi::Paint; use crate::http::{uri, Method, MediaType}; use crate::route::{Handler, RouteUri, BoxFuture}; use crate::sentinel::Sentry; +use crate::doc::Schema; /// A request handling route. /// @@ -190,6 +191,9 @@ pub struct Route { pub format: Option, /// The discovered sentinels. pub(crate) sentinels: Vec, + /// The route's docstring, which may be empty. + pub docstring: &'static str, + } impl Route { @@ -253,6 +257,7 @@ impl Route { sentinels: Vec::new(), handler: Box::new(handler), rank, uri, method, + docstring: "", } } @@ -345,6 +350,11 @@ pub struct StaticInfo { /// Route-derived sentinels, if any. /// This isn't `&'static [SentryInfo]` because `type_name()` isn't `const`. pub sentinels: Vec, + /// The schemas corresponding to the request guards and responder types. + pub schemas: Vec, + /// The doc comment associated with this route. + pub docstring: &'static str, + } #[doc(hidden)] @@ -361,6 +371,7 @@ impl From for Route { format: info.format, sentinels: info.sentinels.into_iter().collect(), uri, + docstring: info.docstring, } } }