From 4ecf55806605042326af9d617e79005e69b4c185 Mon Sep 17 00:00:00 2001
From: Georg Semmler <Georg_semmler_05@web.de>
Date: Sun, 11 Nov 2018 23:58:50 +0100
Subject: [PATCH] Use doc comments instead of the doc attribute in the
 changelog examples (#273)

---
 changelog/master.md               | 17 +++---
 juniper/src/macros/common.rs      |  4 +-
 juniper/src/macros/interface.rs   |  2 +-
 juniper/src/macros/object.rs      | 68 ++++++++---------------
 juniper/src/macros/tests/field.rs | 39 +++++++-------
 juniper/src/schema/meta.rs        | 90 ++++++++++++++++++-------------
 6 files changed, 102 insertions(+), 118 deletions(-)

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>(),
+    )
 }