From 18b559a839681d23dbe4389b24aa5718233f8a28 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Sat, 25 Feb 2017 23:57:27 +0100 Subject: [PATCH 1/2] Provide a block macro for including an operation's snippets See gh-323 Closes gh-354 --- spring-restdocs-asciidoctor/build.gradle | 9 ++ .../RestDocsExtensionRegistry.java | 13 +- .../extensions/operation_block_macro.rb | 115 +++++++++++++++++ .../OperationIncludeBlockMacroTests.java | 121 ++++++++++++++++++ .../resources/operations/all_snippets.html | 31 +++++ .../operations/multiple_snippets.html | 21 +++ .../operations/snippet_custom_title.html | 1 + .../operations/snippet_in_section.html | 13 ++ .../resources/operations/snippet_simple.html | 10 ++ .../resources/operations/snippet_warning.html | 11 ++ .../operations/snippet_with_level.html | 8 ++ .../some-operation/curl-request.adoc | 4 + .../some-operation/custom-snippet.adoc | 4 + .../some-operation/http-request.adoc | 6 + 14 files changed, 365 insertions(+), 2 deletions(-) create mode 100644 spring-restdocs-asciidoctor/src/main/resources/extensions/operation_block_macro.rb create mode 100644 spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/OperationIncludeBlockMacroTests.java create mode 100644 spring-restdocs-asciidoctor/src/test/resources/operations/all_snippets.html create mode 100644 spring-restdocs-asciidoctor/src/test/resources/operations/multiple_snippets.html create mode 100644 spring-restdocs-asciidoctor/src/test/resources/operations/snippet_custom_title.html create mode 100644 spring-restdocs-asciidoctor/src/test/resources/operations/snippet_in_section.html create mode 100644 spring-restdocs-asciidoctor/src/test/resources/operations/snippet_simple.html create mode 100644 spring-restdocs-asciidoctor/src/test/resources/operations/snippet_warning.html create mode 100644 spring-restdocs-asciidoctor/src/test/resources/operations/snippet_with_level.html create mode 100644 spring-restdocs-asciidoctor/src/test/resources/some-operation/curl-request.adoc create mode 100644 spring-restdocs-asciidoctor/src/test/resources/some-operation/custom-snippet.adoc create mode 100644 spring-restdocs-asciidoctor/src/test/resources/some-operation/http-request.adoc diff --git a/spring-restdocs-asciidoctor/build.gradle b/spring-restdocs-asciidoctor/build.gradle index 6897242e..ab311ad5 100644 --- a/spring-restdocs-asciidoctor/build.gradle +++ b/spring-restdocs-asciidoctor/build.gradle @@ -5,3 +5,12 @@ dependencies { testCompile 'junit:junit' testCompile 'org.asciidoctor:asciidoctorj' } + +task copyTestSnippets(type: Copy) { + from 'src/test/resources/some-operation' + into 'build/generated-snippets/some-operation' +} + +test { + dependsOn copyTestSnippets +} 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..6d2be185 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 @@ -17,6 +17,8 @@ package org.springframework.restdocs.asciidoctor; import org.asciidoctor.Asciidoctor; +import org.asciidoctor.extension.JavaExtensionRegistry; +import org.asciidoctor.extension.RubyExtensionRegistry; import org.asciidoctor.extension.spi.ExtensionRegistry; /** @@ -28,8 +30,15 @@ public final class RestDocsExtensionRegistry implements ExtensionRegistry { @Override public void register(Asciidoctor asciidoctor) { - asciidoctor.javaExtensionRegistry() - .preprocessor(new DefaultAttributesPreprocessor()); + JavaExtensionRegistry registry = asciidoctor.javaExtensionRegistry(); + registry.preprocessor(new DefaultAttributesPreprocessor()); + + RubyExtensionRegistry rubyExtensionRegistry = asciidoctor.rubyExtensionRegistry(); + 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..a38fcbc2 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/main/resources/extensions/operation_block_macro.rb @@ -0,0 +1,115 @@ +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', level=] +# +class OperationBlockMacro < Asciidoctor::Extensions::BlockMacroProcessor + use_dsl + named :operation + + def initialize name, config + super + # pre-defined section titles for commonly used snippets + @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' + } + end + + def process(parent, reader, attrs) + doc = parent.document + snippet_dir = doc.attributes['snippets'] + snippets = snippets_to_include(attrs, snippet_dir, reader) + section_level = parent.level + 1 + + params = {:snippet_dir => snippet_dir, + :section_level => section_level, + :operation => reader} + + content = StringIO.new + snippets.each do |snippet| + append_snippet_block(content, snippet, params) + end + + add_snippets_block(content, doc, parent) unless content.length == 0 + nil + end + + def add_snippets_block(content, doc, parent) + fragment = Asciidoctor.load content, + safe: doc.options[:safe], + attributes: {'fragment' => '', 'projectdir' => doc.attr(:projectdir)} + fragment.blocks.each do |b| + b.parent = parent + parent << b + end + end + + def snippets_to_include(attrs, snippet_dir, operation) + if not attrs['snippets'].to_s.empty? + snippets_from_attribute attrs + else + all_snippets snippet_dir, operation + end + end + + def snippets_from_attribute(attrs) + attrs.fetch('snippets').split(',') + end + + def all_snippets(snippet_dir, operation) + all_snippet_file_names = [] + Dir.entries(File.join(snippet_dir.to_s, operation)).sort.select { |file| + if file.end_with? '.adoc' + file.slice!('.adoc') + all_snippet_file_names << file + end + } + all_snippet_file_names + end + + def append_snippet_block(content, snippet, params) + write_title content, snippet, params[:section_level] + write_content content, snippet, params + end + + def write_content(content, snippet, params) + snippet_path = File.join(params[:snippet_dir].to_s, params[:operation], "#{snippet}.adoc") + content.puts File.readlines(snippet_path).join + + rescue Errno::ENOENT + content.puts "WARNING: snippet not found: #{snippet_path}" + add_new_line content + end + + def write_title(content, snippet, level) + # an asciidoctor level is always an equal + # sign more than the level number + section_level = '=' * (level + 1) + content.puts "#{section_level} #{title(snippet)}" + add_new_line content + end + + def title(snippet) + (@titles[snippet.to_sym] || title_from_file_name(snippet)) + end + + def add_new_line(content) + content.puts '' + end + + def title_from_file_name(snippet) + snippet.sub('-', ' ').capitalize + end + +end diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/OperationIncludeBlockMacroTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/OperationIncludeBlockMacroTests.java new file mode 100644 index 00000000..0d952dba --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/OperationIncludeBlockMacroTests.java @@ -0,0 +1,121 @@ +/* + * 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.Test; + +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 based rest docs block macro. + * Because there is no java implementation (yet) + * we can only test the behaviour when rendering. + * + * @author Gerrit Meier + */ +public class OperationIncludeBlockMacroTests { + + private final Options options = new Options(); + private final Asciidoctor asciidoctor = Asciidoctor.Factory.create(); + + @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("snippet_warning"))); + } + + @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"))); + } + + 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..7ecff6a0 --- /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/multiple_snippets.html b/spring-restdocs-asciidoctor/src/test/resources/operations/multiple_snippets.html new file mode 100644 index 00000000..b8c018da --- /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..fe3ce739 --- /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..222c30a2 --- /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_warning.html b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet_warning.html new file mode 100644 index 00000000..1058ad72 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet_warning.html @@ -0,0 +1,11 @@ +
+

Unknown snippet

+
+
+ + + +
+
Warning
+
+snippet not found: \ 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..4a393cda --- /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 From 826e0a5dec8ef0fbc21b01e3123806bee8b682c3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 7 Mar 2017 16:46:35 +0000 Subject: [PATCH 2/2] =?UTF-8?q?Polish=20=E2=80=9CProvide=20a=20block=20mac?= =?UTF-8?q?ro=20for=20including=20an=20operation's=20snippets=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Copy snippets used in the tests in @BeforeClass rather than using Gradle so that the tests can be run easily in an IDE - Address problems in operation_block_macro.rb reported by Rubocop - Rename new test class to more closely match the name of the Ruby class that it’s testing - Gracefully handle a missing operation - Align behaviour when an operation or snippet is missing more closely with Asciidoctor’s behaviour when an include references a missing file - Use kebab-case rather than snake_case for new test resources - Update the documentation to describe the new macro See gh-354 Closes gh-323 --- docs/src/docs/asciidoc/getting-started.adoc | 6 +- .../asciidoc/working-with-asciidoctor.adoc | 33 +++- spring-restdocs-asciidoctor/build.gradle | 12 +- .../RestDocsExtensionRegistry.java | 10 +- .../extensions/operation_block_macro.rb | 163 +++++++++--------- ...sts.java => OperationBlockMacroTests.java} | 69 +++++--- .../{all_snippets.html => all-snippets.html} | 2 +- .../operations/missing-operation.html | 3 + .../resources/operations/missing-snippet.html | 8 + ...e_snippets.html => multiple-snippets.html} | 2 +- ...m_title.html => snippet-custom-title.html} | 0 ...n_section.html => snippet-in-section.html} | 2 +- ...nippet_simple.html => snippet-simple.html} | 2 +- ...ith_level.html => snippet-with-level.html} | 2 +- .../resources/operations/snippet_warning.html | 11 -- 15 files changed, 185 insertions(+), 140 deletions(-) rename spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/{OperationIncludeBlockMacroTests.java => OperationBlockMacroTests.java} (64%) rename spring-restdocs-asciidoctor/src/test/resources/operations/{all_snippets.html => all-snippets.html} (95%) create mode 100644 spring-restdocs-asciidoctor/src/test/resources/operations/missing-operation.html create mode 100644 spring-restdocs-asciidoctor/src/test/resources/operations/missing-snippet.html rename spring-restdocs-asciidoctor/src/test/resources/operations/{multiple_snippets.html => multiple-snippets.html} (92%) rename spring-restdocs-asciidoctor/src/test/resources/operations/{snippet_custom_title.html => snippet-custom-title.html} (100%) rename spring-restdocs-asciidoctor/src/test/resources/operations/{snippet_in_section.html => snippet-in-section.html} (87%) rename spring-restdocs-asciidoctor/src/test/resources/operations/{snippet_simple.html => snippet-simple.html} (85%) rename spring-restdocs-asciidoctor/src/test/resources/operations/{snippet_with_level.html => snippet-with-level.html} (83%) delete mode 100644 spring-restdocs-asciidoctor/src/test/resources/operations/snippet_warning.html 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 ab311ad5..46da4c8f 100644 --- a/spring-restdocs-asciidoctor/build.gradle +++ b/spring-restdocs-asciidoctor/build.gradle @@ -4,13 +4,5 @@ dependencies { compileOnly 'org.asciidoctor:asciidoctorj' testCompile 'junit:junit' testCompile 'org.asciidoctor:asciidoctorj' -} - -task copyTestSnippets(type: Copy) { - from 'src/test/resources/some-operation' - into 'build/generated-snippets/some-operation' -} - -test { - dependsOn copyTestSnippets -} + 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 6d2be185..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 @@ -17,8 +17,6 @@ package org.springframework.restdocs.asciidoctor; import org.asciidoctor.Asciidoctor; -import org.asciidoctor.extension.JavaExtensionRegistry; -import org.asciidoctor.extension.RubyExtensionRegistry; import org.asciidoctor.extension.spi.ExtensionRegistry; /** @@ -30,11 +28,9 @@ public final class RestDocsExtensionRegistry implements ExtensionRegistry { @Override public void register(Asciidoctor asciidoctor) { - JavaExtensionRegistry registry = asciidoctor.javaExtensionRegistry(); - registry.preprocessor(new DefaultAttributesPreprocessor()); - - RubyExtensionRegistry rubyExtensionRegistry = asciidoctor.rubyExtensionRegistry(); - rubyExtensionRegistry + 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 index a38fcbc2..c1f70f5c 100644 --- a/spring-restdocs-asciidoctor/src/main/resources/extensions/operation_block_macro.rb +++ b/spring-restdocs-asciidoctor/src/main/resources/extensions/operation_block_macro.rb @@ -1,115 +1,124 @@ require 'asciidoctor/extensions' require 'stringio' -# Spring REST Docs block macro to import multiple snippet of an operation at once +# Spring REST Docs block macro to import multiple snippet of an operation at +# once # # Usage # -# operation::operation-name[snippets='snippet-name1,snippet-name2', level=] +# operation::operation-name[snippets='snippet-name1,snippet-name2'] # class OperationBlockMacro < Asciidoctor::Extensions::BlockMacroProcessor use_dsl named :operation - def initialize name, config - super - # pre-defined section titles for commonly used snippets - @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' - } - end - - def process(parent, reader, attrs) - doc = parent.document - snippet_dir = doc.attributes['snippets'] - snippets = snippets_to_include(attrs, snippet_dir, reader) - section_level = parent.level + 1 - - params = {:snippet_dir => snippet_dir, - :section_level => section_level, - :operation => reader} - - content = StringIO.new - snippets.each do |snippet| - append_snippet_block(content, snippet, params) - end - - add_snippets_block(content, doc, parent) unless content.length == 0 + 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) - fragment = Asciidoctor.load content, - safe: doc.options[:safe], - attributes: {'fragment' => '', 'projectdir' => doc.attr(:projectdir)} + 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(attrs, snippet_dir, operation) - if not attrs['snippets'].to_s.empty? - snippets_from_attribute attrs + def snippets_to_include(snippet_names, snippets_dir, operation) + if snippet_names.empty? + all_snippets snippets_dir, operation else - all_snippets snippet_dir, operation + snippet_names.split(',').map do |name| + path = File.join snippets_dir, operation, "#{name}.adoc" + Snippet.new(path, name) + end end end - def snippets_from_attribute(attrs) - attrs.fetch('snippets').split(',') + 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 all_snippets(snippet_dir, operation) - all_snippet_file_names = [] - Dir.entries(File.join(snippet_dir.to_s, operation)).sort.select { |file| - if file.end_with? '.adoc' - file.slice!('.adoc') - all_snippet_file_names << file - end - } - all_snippet_file_names + def append_snippet_block(content, snippet, section_level, operation) + write_title content, snippet, section_level + write_content content, snippet, operation end - def append_snippet_block(content, snippet, params) - write_title content, snippet, params[:section_level] - write_content content, snippet, params - end - - def write_content(content, snippet, params) - snippet_path = File.join(params[:snippet_dir].to_s, params[:operation], "#{snippet}.adoc") - content.puts File.readlines(snippet_path).join - - rescue Errno::ENOENT - content.puts "WARNING: snippet not found: #{snippet_path}" - add_new_line content + 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) - # an asciidoctor level is always an equal - # sign more than the level number section_level = '=' * (level + 1) - content.puts "#{section_level} #{title(snippet)}" - add_new_line content - end - - def title(snippet) - (@titles[snippet.to_sym] || title_from_file_name(snippet)) - end - - def add_new_line(content) + content.puts "#{section_level} #{snippet.title}" content.puts '' end - def title_from_file_name(snippet) - snippet.sub('-', ' ').capitalize - 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/OperationIncludeBlockMacroTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/OperationBlockMacroTests.java similarity index 64% rename from spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/OperationIncludeBlockMacroTests.java rename to spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/OperationBlockMacroTests.java index 0d952dba..786a8dd5 100644 --- a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/OperationIncludeBlockMacroTests.java +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/OperationBlockMacroTests.java @@ -27,25 +27,36 @@ 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 based rest docs block macro. - * Because there is no java implementation (yet) - * we can only test the behaviour when rendering. + * Tests for Ruby operation block macro. * * @author Gerrit Meier + * @author Andy Wilkinson */ -public class OperationIncludeBlockMacroTests { +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()); @@ -61,61 +72,65 @@ public class OperationIncludeBlockMacroTests { public void simpleSnippetInclude() throws Exception { String result = this.asciidoctor.convert( "operation::some-operation[snippets='curl-request']", this.options); - - assertThat(result, equalTo(getExpectedContentFromFile("snippet_simple"))); + 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"))); + "== 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"))); + "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"))); + 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"))); + 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("snippet_warning"))); + 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"))); + assertThat(result, + containsString(getExpectedContentFromFile("snippet-custom-title"))); } - private String getExpectedContentFromFile(String fileName) throws URISyntaxException, IOException { - Path filePath = Paths.get(this.getClass().getResource("/operations/" + fileName + ".html").toURI()); + @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 similarity index 95% rename from spring-restdocs-asciidoctor/src/test/resources/operations/all_snippets.html rename to spring-restdocs-asciidoctor/src/test/resources/operations/all-snippets.html index 7ecff6a0..9cb2f024 100644 --- a/spring-restdocs-asciidoctor/src/test/resources/operations/all_snippets.html +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/all-snippets.html @@ -1,5 +1,5 @@
-

curl request

+

Curl request

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 similarity index 92% rename from spring-restdocs-asciidoctor/src/test/resources/operations/multiple_snippets.html rename to spring-restdocs-asciidoctor/src/test/resources/operations/multiple-snippets.html index b8c018da..89b8ffba 100644 --- a/spring-restdocs-asciidoctor/src/test/resources/operations/multiple_snippets.html +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/multiple-snippets.html @@ -1,5 +1,5 @@
-

curl request

+

Curl request

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 similarity index 100% rename from spring-restdocs-asciidoctor/src/test/resources/operations/snippet_custom_title.html rename to spring-restdocs-asciidoctor/src/test/resources/operations/snippet-custom-title.html 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 similarity index 87% rename from spring-restdocs-asciidoctor/src/test/resources/operations/snippet_in_section.html rename to spring-restdocs-asciidoctor/src/test/resources/operations/snippet-in-section.html index fe3ce739..fd682f4b 100644 --- a/spring-restdocs-asciidoctor/src/test/resources/operations/snippet_in_section.html +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-in-section.html @@ -2,7 +2,7 @@

Section

-

curl request

+

Curl request

$ curl 'http://localhost:8080/' -i
diff --git a/spring-restdocs-asciidoctor/src/test/resources/operations/snippet_simple.html b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-simple.html similarity index 85% rename from spring-restdocs-asciidoctor/src/test/resources/operations/snippet_simple.html rename to spring-restdocs-asciidoctor/src/test/resources/operations/snippet-simple.html index 222c30a2..35a3fa54 100644 --- a/spring-restdocs-asciidoctor/src/test/resources/operations/snippet_simple.html +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-simple.html @@ -1,5 +1,5 @@
-

curl request

+

Curl request

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 similarity index 83% rename from spring-restdocs-asciidoctor/src/test/resources/operations/snippet_with_level.html rename to spring-restdocs-asciidoctor/src/test/resources/operations/snippet-with-level.html index 4a393cda..727ccf02 100644 --- a/spring-restdocs-asciidoctor/src/test/resources/operations/snippet_with_level.html +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-with-level.html @@ -1,5 +1,5 @@
-

curl request

+

Curl request

$ curl 'http://localhost:8080/' -i
diff --git a/spring-restdocs-asciidoctor/src/test/resources/operations/snippet_warning.html b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet_warning.html deleted file mode 100644 index 1058ad72..00000000 --- a/spring-restdocs-asciidoctor/src/test/resources/operations/snippet_warning.html +++ /dev/null @@ -1,11 +0,0 @@ -
-

Unknown snippet

-
-
- - - -
-
Warning
-
-snippet not found: \ No newline at end of file