diff --git a/changelog/master.md b/changelog/master.md index 45017053..6448e8d9 100644 --- a/changelog/master.md +++ b/changelog/master.md @@ -57,13 +57,13 @@ } // New alternative syntax for field descriptions - #[doc = "Description"] + /// Description field my_field() -> { ... } // New alternative syntax for argument descriptions field my_field( - #[doc = "The number of starfish to be returned. \ - Can't be more than 100."] + /// The number of starfish to be returned. + /// Can't be more than 100. arg: i32, ) -> { ... @@ -73,12 +73,11 @@ // // Multiple docstrings will be collapsed into a single // description separated by newlines. - #[doc = r#" - This is my field. - - Make sure not to flitz the bitlet. - Flitzing without a bitlet has undefined behaviour. - "] + /// This is my field. + /// + /// Make sure not to filtz the bitlet. + /// Flitzing without a bitlet has undefined behaviour. + /// #[doc = my_consts::ADDED_IN_VERSION_XYZ] field my_field() -> { ... } ``` diff --git a/juniper/src/macros/common.rs b/juniper/src/macros/common.rs index f8c27e0e..733ce93b 100644 --- a/juniper/src/macros/common.rs +++ b/juniper/src/macros/common.rs @@ -677,7 +677,7 @@ macro_rules! __juniper_create_arg { $info, ) $(.description($arg_description))* - $(.push_docstring($arg_docstring))* + .push_docstring(&[$($arg_docstring,)*]) }; ( @@ -695,6 +695,6 @@ macro_rules! __juniper_create_arg { $info, ) $(.description($arg_description))* - $(.push_docstring($arg_docstring))* + .push_docstring(&[$($arg_docstring,)*]) }; } diff --git a/juniper/src/macros/interface.rs b/juniper/src/macros/interface.rs index c1e2a419..43559841 100644 --- a/juniper/src/macros/interface.rs +++ b/juniper/src/macros/interface.rs @@ -153,7 +153,7 @@ macro_rules! graphql_interface { info ) $(.description($fn_description))* - $(.push_docstring($docstring))* + .push_docstring(&[$($docstring,)*]) $(.deprecated($deprecated))* $(.argument( __juniper_create_arg!( diff --git a/juniper/src/macros/object.rs b/juniper/src/macros/object.rs index c077deae..310946a7 100644 --- a/juniper/src/macros/object.rs +++ b/juniper/src/macros/object.rs @@ -35,8 +35,11 @@ graphql_object!(User: () |&self| { ## Documentation and descriptions -You can optionally add descriptions to the type itself, the fields, and field -arguments: +You can optionally add descriptions to the type itself, the fields, +and field arguments. For field and argument descriptions it is +possible to use normal rustdoc comments or doc +attributes. Alternatively the same syntax as for the type could be +used ```rust # #[macro_use] extern crate juniper; @@ -45,7 +48,9 @@ struct User { id: String, name: String, group_ids: Vec<String> } graphql_object!(User: () |&self| { description: "A user in the database" - field id() -> &String as "The user's unique identifier" { + + /// The user's unique identifier + field id() -> &String { &self.id } @@ -53,47 +58,11 @@ graphql_object!(User: () |&self| { &self.name } + #[doc = "Test if a user is member of a group"] field member_of_group( - group_id: String as "The group id you want to test membership against" - ) -> bool as "Test if a user is member of a group" { - self.group_ids.iter().any(|gid| gid == &group_id) - } -}); - -# fn main() { } -``` - -**Alternatively,** descriptions can be added with the builtin `doc` attribute. -Consecutive `#[doc = "..."]` attributes will be collapsed into a single description -where the docstrings are separated by newlines. - -```rust -# #[macro_use] extern crate juniper; -struct User { id: String, name: String, group_ids: Vec<String> } - -graphql_object!(User: () |&self| { - description: "A user in the database" - - #[doc = "The user's unique identifier"] - field id() -> &String { - &self.id - } - - #[doc = "The user's name"] - field name() -> &String { - &self.name - } - - #[doc = r#" - Test if a user is member of a group. - - This may return a flitzbit if the floop is twizled. - Make sure not to rumblejumble the cog-rotater. - "#] - #[doc = "Added in vX.Y.44"] - field member_of_group( - #[doc = "The group id you want to test membership against"] - group_id: String, + /// The group id you want to test membership against + /// second line + group_id: String ) -> bool { self.group_ids.iter().any(|gid| gid == &group_id) } @@ -275,16 +244,19 @@ A field's description and deprecation can also be set using the builtin `doc` and `deprecated` attributes. ```text -#[doc = "Field description"] +/// Field description field name(args...) -> Type { } +#[doc = "Field description"] +field name(args...) -> Type {} + #[deprecated] // no reason required field name(args...) -> Type { } #[deprecated(note = "Reason")] field name(args...) -> Type { } -#[doc = "Field description"] +/// Field description #[deprecated(note = "Reason")] // deprecated must come after doc field deprecated "Reason" name(args...) -> Type { } ``` @@ -326,9 +298,11 @@ arg_name = (Point { x: 1, y: 2 }): Point arg_name = ("default".to_owned()): String ``` -A description can also be provided using the builtin `doc` attribute. +A description can also be provided using normal doc comments or doc attributes. ```text +/// Argument description +arg_name: ArgType #[doc = "Argument description"] arg_name: ArgType ``` @@ -391,7 +365,7 @@ macro_rules! graphql_object { info ) $(.description($fn_description))* - $(.push_docstring($docstring))* + .push_docstring(&[$($docstring,)*]) $(.deprecated($deprecated))* $(.argument( __juniper_create_arg!( diff --git a/juniper/src/macros/tests/field.rs b/juniper/src/macros/tests/field.rs index 82b428d4..220ed411 100644 --- a/juniper/src/macros/tests/field.rs +++ b/juniper/src/macros/tests/field.rs @@ -31,19 +31,18 @@ graphql_object!(Root: () |&self| { field deprecated "Deprecation reason" deprecated_descr() -> i32 as "Field description" { 0 } - #[doc = "Field description"] + /// Field description field attr_description() -> i32 { 0 } - #[doc = "Field description"] - #[doc = "with `collapse_docs` behavior"] // https://doc.rust-lang.org/rustdoc/the-doc-attribute.html + /// Field description + /// with `collapse_docs` behavior field attr_description_collapse() -> i32 { 0 } - #[doc = r#" - Get the i32 representation of 0. - - - This comment is longer. - - These two lines are rendered as bullets by GraphiQL. - "#] + /// Get the i32 representation of 0. + /// + /// - This comment is longer. + /// - These two lines are rendered as bullets by GraphiQL. + /// - subsection field attr_description_long() -> i32 { 0 } #[deprecated] @@ -52,7 +51,7 @@ graphql_object!(Root: () |&self| { #[deprecated(note = "Deprecation reason")] field attr_deprecated_reason() -> i32 { 0 } - #[doc = "Field description"] + /// Field description #[deprecated(note = "Deprecation reason")] field attr_deprecated_descr() -> i32 { 0 } @@ -76,19 +75,17 @@ graphql_interface!(Interface: () |&self| { field deprecated "Deprecation reason" deprecated_descr() -> i32 as "Field description" { 0 } - #[doc = "Field description"] + /// Field description field attr_description() -> i32 { 0 } - #[doc = "Field description"] - #[doc = "with `collapse_docs` behavior"] // https://doc.rust-lang.org/rustdoc/the-doc-attribute.html + /// Field description + /// with `collapse_docs` behavior field attr_description_collapse() -> i32 { 0 } - #[doc = r#" - Get the i32 representation of 0. - - - This comment is longer. - - These two lines are rendered as bullets by GraphiQL. - "#] + /// Get the i32 representation of 0. + /// + /// - This comment is longer. + /// - These two lines are rendered as bullets by GraphiQL. field attr_description_long() -> i32 { 0 } #[deprecated] @@ -97,7 +94,7 @@ graphql_interface!(Interface: () |&self| { #[deprecated(note = "Deprecation reason")] field attr_deprecated_reason() -> i32 { 0 } - #[doc = "Field description"] + /// Field description #[deprecated(note = "Deprecation reason")] field attr_deprecated_descr() -> i32 { 0 } @@ -384,7 +381,7 @@ fn introspect_object_field_attr_description_long() { ); assert_eq!( field.get_field_value("description"), - Some(&Value::scalar("Get the i32 representation of 0.\n\n- This comment is longer.\n- These two lines are rendered as bullets by GraphiQL.")) + Some(&Value::scalar("Get the i32 representation of 0.\n\n- This comment is longer.\n- These two lines are rendered as bullets by GraphiQL.\n - subsection")) ); assert_eq!( field.get_field_value("isDeprecated"), diff --git a/juniper/src/schema/meta.rs b/juniper/src/schema/meta.rs index 93a4095d..362981a0 100644 --- a/juniper/src/schema/meta.rs +++ b/juniper/src/schema/meta.rs @@ -613,15 +613,16 @@ impl<'a, S> Field<'a, S> { /// /// If the description hasn't been set, the description is set to the provided line. /// Otherwise, the doc string is added to the current description after a newline. - pub fn push_docstring(mut self, multiline: &str) -> Field<'a, S> { - let docstring = clean_docstring(multiline); - match &mut self.description { - &mut Some(ref mut desc) => { - desc.push('\n'); - desc.push_str(&docstring); - } - desc @ &mut None => { - *desc = Some(docstring.to_string()); + pub fn push_docstring(mut self, multiline: &[&str]) -> Field<'a, S> { + if let Some(docstring) = clean_docstring(multiline) { + match &mut self.description { + &mut Some(ref mut desc) => { + desc.push('\n'); + desc.push_str(&docstring); + } + desc @ &mut None => { + *desc = Some(docstring); + } } } self @@ -679,15 +680,16 @@ impl<'a, S> Argument<'a, S> { /// /// If the description hasn't been set, the description is set to the provided line. /// Otherwise, the doc string is added to the current description after a newline. - pub fn push_docstring(mut self, multiline: &str) -> Argument<'a, S> { - let docstring = clean_docstring(multiline); - match &mut self.description { - &mut Some(ref mut desc) => { - desc.push('\n'); - desc.push_str(&docstring); - } - desc @ &mut None => { - *desc = Some(docstring.to_string()); + pub fn push_docstring(mut self, multiline: &[&str]) -> Argument<'a, S> { + if let Some(docstring) = clean_docstring(multiline) { + match &mut self.description { + &mut Some(ref mut desc) => { + desc.push('\n'); + desc.push_str(&docstring); + } + desc @ &mut None => { + *desc = Some(docstring) + } } } self @@ -766,26 +768,38 @@ where <T as FromInputValue<S>>::from_input_value(v).is_some() } -fn clean_docstring<'a>(multiline: &'a str) -> Cow<'a, str> { - let trim_start = multiline.split('\n') - .skip(1) - .filter_map(|ln| ln.chars().position(|ch| ch != ' ' && ch != '\t')) - .min(); - if let Some(trim) = trim_start { - let trimmed = multiline - .split('\n') - .map(|ln| { - if !ln.starts_with(' ') && !ln.starts_with('\t') { - ln // skip trimming the first line - } else if ln.len() >= trim { - &ln[trim..] +fn clean_docstring(multiline: &[&str]) -> Option<String> { + if multiline.is_empty() { + return None; + } + let trim_start = multiline + .iter() + .filter_map(|ln| ln.chars().position(|ch| !ch.is_whitespace())) + .min() + .unwrap_or(0); + Some( + multiline + .iter() + .enumerate() + .flat_map(|(line, ln)| { + let new_ln = if !ln + .chars() + .next() + .map(|ch| ch.is_whitespace()) + .unwrap_or(false) + { + ln.trim_end() // skip trimming the first line + } else if ln.len() >= trim_start { + &ln[trim_start..].trim_end() } else { "" - } - }) - .collect::<Vec<_>>(); - Cow::from(trimmed.join("\n").trim_matches('\n').to_owned()) - } else { - Cow::from(multiline.trim_matches('\n')) - } + }; + new_ln.chars().chain( + ['\n'] + .iter() + .take_while(move |_| line < multiline.len() - 1) + .cloned(), + ) + }).collect::<String>(), + ) }