diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index f84f0f66..1b72d5f0 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -143,7 +143,8 @@ the configuration are described below. <>. <5> Add `spring-restdocs-asciidoctor` as a dependency of the Asciidoctor plugin. This will automatically configure the `snippets` attribute for use in your `.adoc` files to - point to `target/generated-snippets`. + point to `target/generated-snippets`. It will also allow you to use the `operation` + block macro. [source,indent=0,subs="verbatim,attributes",role="secondary"] .Gradle @@ -173,7 +174,8 @@ the configuration are described below. <1> Apply the Asciidoctor plugin. <2> Add a dependency on `spring-restdocs-asciidoctor` in the `asciidoctor` configuration. This will automatically configure the `snippets` attribute for use in your `.adoc` - files to point to `build/generated-snippets`. + files to point to `build/generated-snippets`. It will also allow you to use the + `operation` block macro. <3> Add a dependency on `spring-restdocs-mockmvc` in the `testCompile` configuration. If you want to use REST Assured rather than MockMvc, add a dependency on `spring-restdocs-restassured` instead. diff --git a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc index 008ab6f9..982ca369 100644 --- a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc +++ b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc @@ -17,8 +17,39 @@ relevant to Spring REST Docs. [[working-with-asciidoctor-including-snippets]] === Including snippets +[[working-with-asciidoctor-including-snippets-operation]] +==== Including multiple snippets for an operation + +A macro named `operation` can be used to import all or some of the snippets that have +been generated for a specific operation. It is made available by including +`spring-restdocs-asciidoctor` in your project's <>. + +The target of the macro is the name of the operation. The `snippets` attribute can be +used to select the snippets that should be included using a comma-separated list. +Each entry in the list should be the name of a snippet file, minus the `.adoc` suffix, +to include. For example, to include the curl, HTTP request and HTTP response snippets +for the index operation: + +[source,indent=0] +---- +operation::index[snippets=curl-request,http-request,http-response] +---- + +To include all of an operation's snippets, the `snippets` attribute can be omitted: + +[source,indent=0] +---- +operation::index[] +---- + + + +[[working-with-asciidoctor-including-snippets-individual]] +==== Including individual snippets + The http://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#include-files[include -macro] is used to include generated snippets in your documentation. The `snippets` +macro] is used to include individual snippets in your documentation. The `snippets` attribute that is automatically set by `spring-restdocs-asciidoctor` configured in the <> can be used to reference the snippets output directory. For example: diff --git a/spring-restdocs-asciidoctor/build.gradle b/spring-restdocs-asciidoctor/build.gradle index 6897242e..46da4c8f 100644 --- a/spring-restdocs-asciidoctor/build.gradle +++ b/spring-restdocs-asciidoctor/build.gradle @@ -4,4 +4,5 @@ dependencies { compileOnly 'org.asciidoctor:asciidoctorj' testCompile 'junit:junit' testCompile 'org.asciidoctor:asciidoctorj' -} + testCompile 'org.springframework:spring-core' +} \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java index 15bca154..a0b4c766 100644 --- a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java @@ -30,6 +30,11 @@ public final class RestDocsExtensionRegistry implements ExtensionRegistry { public void register(Asciidoctor asciidoctor) { asciidoctor.javaExtensionRegistry() .preprocessor(new DefaultAttributesPreprocessor()); + asciidoctor.rubyExtensionRegistry() + .loadClass(RestDocsExtensionRegistry.class + .getResourceAsStream("/extensions/operation_block_macro.rb")) + .blockMacro("operation", "OperationBlockMacro"); + } } diff --git a/spring-restdocs-asciidoctor/src/main/resources/extensions/operation_block_macro.rb b/spring-restdocs-asciidoctor/src/main/resources/extensions/operation_block_macro.rb new file mode 100644 index 00000000..c1f70f5c --- /dev/null +++ b/spring-restdocs-asciidoctor/src/main/resources/extensions/operation_block_macro.rb @@ -0,0 +1,124 @@ +require 'asciidoctor/extensions' +require 'stringio' + +# Spring REST Docs block macro to import multiple snippet of an operation at +# once +# +# Usage +# +# operation::operation-name[snippets='snippet-name1,snippet-name2'] +# +class OperationBlockMacro < Asciidoctor::Extensions::BlockMacroProcessor + use_dsl + named :operation + + def process(parent, operation, attributes) + snippets_dir = parent.document.attributes['snippets'].to_s + snippet_names = attributes.fetch 'snippets', '' + content = read_snippets(snippets_dir, snippet_names, parent.level + 1, + operation) + add_snippets_block(content, parent.document, parent) unless content.empty? + nil + end + + def read_snippets(snippets_dir, snippet_names, section_level, operation) + snippets = snippets_to_include(snippet_names, snippets_dir, operation) + if snippets.empty? + warn "No snippets were found for operation #{operation} in"\ + "#{snippets_dir}" + "No snippets found for operation::#{operation}" + else + do_read_snippets(snippets, section_level, operation) + end + end + + def do_read_snippets(snippets, section_level, operation) + content = StringIO.new + snippets.each do |snippet| + append_snippet_block(content, snippet, section_level, operation) + end + content.string + end + + def add_snippets_block(content, doc, parent) + options = { safe: doc.options[:safe], + attributes: { 'fragment' => '', + 'projectdir' => doc.attr(:projectdir) } } + fragment = Asciidoctor.load content, options + fragment.blocks.each do |b| + b.parent = parent + parent << b + end + end + + def snippets_to_include(snippet_names, snippets_dir, operation) + if snippet_names.empty? + all_snippets snippets_dir, operation + else + snippet_names.split(',').map do |name| + path = File.join snippets_dir, operation, "#{name}.adoc" + Snippet.new(path, name) + end + end + end + + def all_snippets(snippets_dir, operation) + operation_dir = File.join snippets_dir, operation + return [] unless Dir.exist? operation_dir + Dir.entries(operation_dir) + .sort + .select { |file| file.end_with? '.adoc' } + .map { |file| Snippet.new(File.join(operation_dir, file), file[0..-6]) } + end + + def append_snippet_block(content, snippet, section_level, operation) + write_title content, snippet, section_level + write_content content, snippet, operation + end + + def write_content(content, snippet, operation) + if File.file? snippet.path + content.puts File.readlines(snippet.path).join + else + warn "Snippet #{snippet.name} not found at #{snippet.path} for"\ + " operation #{operation}" + content.puts "Snippet #{snippet.name} not found for"\ + " operation::#{operation}" + content.puts '' + end + end + + def write_title(content, snippet, level) + section_level = '=' * (level + 1) + content.puts "#{section_level} #{snippet.title}" + content.puts '' + end + + # Details of a snippet to be rendered + class Snippet + @titles = { 'http-request' => 'HTTP request', + 'curl-request' => 'Curl request', + 'httpie-request' => 'HTTPie request', + 'request-body' => 'Request body', + 'request-fields' => 'Request fields', + 'http-response' => 'HTTP response', + 'response-body' => 'Response body', + 'response-fields' => 'Response fields', + 'links' => 'Links' } + + class << self + attr_reader :titles + end + + attr_reader :name, :path + + def initialize(path, name) + @path = path + @name = name + end + + def title + Snippet.titles.fetch @name, name.sub('-', ' ').capitalize + end + end +end diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/OperationBlockMacroTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/OperationBlockMacroTests.java new file mode 100644 index 00000000..786a8dd5 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/OperationBlockMacroTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2014-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.asciidoctor; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.asciidoctor.Asciidoctor; +import org.asciidoctor.Attributes; +import org.asciidoctor.Options; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.springframework.util.FileSystemUtils; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.junit.Assert.assertThat; + +/** + * Tests for Ruby operation block macro. + * + * @author Gerrit Meier + * @author Andy Wilkinson + */ +public class OperationBlockMacroTests { + + private final Options options = new Options(); + + private final Asciidoctor asciidoctor = Asciidoctor.Factory.create(); + + @BeforeClass + public static void prepareOperationSnippets() throws IOException { + File destination = new File("build/generated-snippets/some-operation"); + destination.mkdirs(); + FileSystemUtils.copyRecursively(new File("src/test/resources/some-operation"), + destination); + } + + @Before + public void setUp() { + this.options.setAttributes(getAttributes()); + } + + private Attributes getAttributes() { + Attributes attributes = new Attributes(); + attributes.setAttribute("projectdir", new File(".").getAbsolutePath()); + return attributes; + } + + @Test + public void simpleSnippetInclude() throws Exception { + String result = this.asciidoctor.convert( + "operation::some-operation[snippets='curl-request']", this.options); + assertThat(result, equalTo(getExpectedContentFromFile("snippet-simple"))); + } + + @Test + public void includeSnippetInSection() throws Exception { + String result = this.asciidoctor.convert( + "== Section\n" + "operation::some-operation[snippets='curl-request']", + this.options); + assertThat(result, equalTo(getExpectedContentFromFile("snippet-in-section"))); + } + + @Test + public void includeMultipleSnippets() throws Exception { + String result = this.asciidoctor.convert( + "operation::some-operation[snippets='curl-request,http-request']", + this.options); + assertThat(result, equalTo(getExpectedContentFromFile("multiple-snippets"))); + } + + @Test + public void useMacroWithoutSnippetAttributeAddsAllSnippets() throws Exception { + String result = this.asciidoctor.convert("operation::some-operation[]", + this.options); + assertThat(result, equalTo(getExpectedContentFromFile("all-snippets"))); + } + + @Test + public void useMacroWithEmptySnippetAttributeAddsAllSnippets() throws Exception { + String result = this.asciidoctor.convert("operation::some-operation[snippets=]", + this.options); + assertThat(result, equalTo(getExpectedContentFromFile("all-snippets"))); + } + + @Test + public void includingUnknownSnippetAddsWarning() throws Exception { + String result = this.asciidoctor.convert( + "operation::some-operation[snippets='unknown-snippet']", this.options); + assertThat(result, startsWith(getExpectedContentFromFile("missing-snippet"))); + } + + @Test + public void includingCustomSnippetCreatesCustomTitle() throws Exception { + String result = this.asciidoctor.convert( + "operation::some-operation[snippets='custom-snippet']", this.options); + assertThat(result, + containsString(getExpectedContentFromFile("snippet-custom-title"))); + } + + @Test + public void nonExistentOperationIsHandledGracefully() throws Exception { + String result = this.asciidoctor.convert("operation::non-existent-operation[]", + this.options); + assertThat(result, startsWith(getExpectedContentFromFile("missing-operation"))); + } + + private String getExpectedContentFromFile(String fileName) + throws URISyntaxException, IOException { + Path filePath = Paths.get( + this.getClass().getResource("/operations/" + fileName + ".html").toURI()); + return new String(Files.readAllBytes(filePath)); + } +} diff --git a/spring-restdocs-asciidoctor/src/test/resources/operations/all-snippets.html b/spring-restdocs-asciidoctor/src/test/resources/operations/all-snippets.html new file mode 100644 index 00000000..9cb2f024 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/all-snippets.html @@ -0,0 +1,31 @@ +
+

Curl request

+
+
+
+
$ curl 'http://localhost:8080/' -i
+
+
+
+
+
+

Custom snippet

+
+
+
+
mycustomsnippet
+
+
+
+
+
+

HTTP request

+
+
+
+
GET / HTTP/1.1
+Host: localhost:8080
+
+
+
+
\ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/operations/missing-operation.html b/spring-restdocs-asciidoctor/src/test/resources/operations/missing-operation.html new file mode 100644 index 00000000..c1b4220f --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/missing-operation.html @@ -0,0 +1,3 @@ +
+

No snippets found for operation::non-existent-operation

+
\ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/operations/missing-snippet.html b/spring-restdocs-asciidoctor/src/test/resources/operations/missing-snippet.html new file mode 100644 index 00000000..b71fcae4 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/missing-snippet.html @@ -0,0 +1,8 @@ +
+

Unknown snippet

+
+
+

Snippet unknown-snippet not found for operation::some-operation

+
+
+
\ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/operations/multiple-snippets.html b/spring-restdocs-asciidoctor/src/test/resources/operations/multiple-snippets.html new file mode 100644 index 00000000..89b8ffba --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/multiple-snippets.html @@ -0,0 +1,21 @@ +
+

Curl request

+
+
+
+
$ curl 'http://localhost:8080/' -i
+
+
+
+
+
+

HTTP request

+
+
+
+
GET / HTTP/1.1
+Host: localhost:8080
+
+
+
+
\ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-custom-title.html b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-custom-title.html new file mode 100644 index 00000000..132a95ce --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-custom-title.html @@ -0,0 +1 @@ +

Custom snippet

diff --git a/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-in-section.html b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-in-section.html new file mode 100644 index 00000000..fd682f4b --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-in-section.html @@ -0,0 +1,13 @@ +
+

Section

+
+
+

Curl request

+
+
+
$ curl 'http://localhost:8080/' -i
+
+
+
+
+
\ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-simple.html b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-simple.html new file mode 100644 index 00000000..35a3fa54 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-simple.html @@ -0,0 +1,10 @@ +
+

Curl request

+
+
+
+
$ curl 'http://localhost:8080/' -i
+
+
+
+
\ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-with-level.html b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-with-level.html new file mode 100644 index 00000000..727ccf02 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-with-level.html @@ -0,0 +1,8 @@ +
+

Curl request

+
+
+
$ curl 'http://localhost:8080/' -i
+
+
+
\ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/some-operation/curl-request.adoc b/spring-restdocs-asciidoctor/src/test/resources/some-operation/curl-request.adoc new file mode 100644 index 00000000..0183405f --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/some-operation/curl-request.adoc @@ -0,0 +1,4 @@ +[source,bash] +---- +$ curl 'http://localhost:8080/' -i +---- \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/some-operation/custom-snippet.adoc b/spring-restdocs-asciidoctor/src/test/resources/some-operation/custom-snippet.adoc new file mode 100644 index 00000000..4a27b9ba --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/some-operation/custom-snippet.adoc @@ -0,0 +1,4 @@ +[source,http,options="nowrap"] +---- +mycustomsnippet +---- \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/some-operation/http-request.adoc b/spring-restdocs-asciidoctor/src/test/resources/some-operation/http-request.adoc new file mode 100644 index 00000000..2034fb60 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/some-operation/http-request.adoc @@ -0,0 +1,6 @@ +[source,http,options="nowrap"] +---- +GET / HTTP/1.1 +Host: localhost:8080 + +---- \ No newline at end of file