Polish “Provide a block macro for including an operation's snippets”

- 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
This commit is contained in:
Andy Wilkinson
2017-03-07 16:46:35 +00:00
parent 18b559a839
commit 826e0a5dec
15 changed files with 185 additions and 140 deletions

View File

@@ -143,7 +143,8 @@ the configuration are described below.
<<getting-started-build-configuration-maven-packaging, included in the package>>.
<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.

View File

@@ -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 <<getting-started-build-configuration,
build configuration>>.
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
<<getting-started-build-configuration, build configuration>> can be used to reference the
snippets output directory. For example:

View File

@@ -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'
}

View File

@@ -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");

View File

@@ -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=<indentation 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

View File

@@ -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));
}
}

View File

@@ -1,5 +1,5 @@
<div class="sect1">
<h2 id="_curl_request">curl request</h2>
<h2 id="_curl_request">Curl request</h2>
<div class="sectionbody">
<div class="listingblock">
<div class="content">

View File

@@ -0,0 +1,3 @@
<div class="paragraph">
<p>No snippets found for operation::non-existent-operation</p>
</div>

View File

@@ -0,0 +1,8 @@
<div class="sect1">
<h2 id="_unknown_snippet">Unknown snippet</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Snippet unknown-snippet not found for operation::some-operation</p>
</div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
<div class="sect1">
<h2 id="_curl_request">curl request</h2>
<h2 id="_curl_request">Curl request</h2>
<div class="sectionbody">
<div class="listingblock">
<div class="content">

View File

@@ -2,7 +2,7 @@
<h2 id="_section">Section</h2>
<div class="sectionbody">
<div class="sect2">
<h3 id="_curl_request">curl request</h3>
<h3 id="_curl_request">Curl request</h3>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">$ curl 'http://localhost:8080/' -i</code></pre>

View File

@@ -1,5 +1,5 @@
<div class="sect1">
<h2 id="_curl_request">curl request</h2>
<h2 id="_curl_request">Curl request</h2>
<div class="sectionbody">
<div class="listingblock">
<div class="content">

View File

@@ -1,5 +1,5 @@
<div class="sect2">
<h3 id="_curl_request">curl request</h3>
<h3 id="_curl_request">Curl request</h3>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">$ curl 'http://localhost:8080/' -i</code></pre>

View File

@@ -1,11 +0,0 @@
<div class="sect1">
<h2 id="_unknown_snippet">Unknown snippet</h2>
<div class="sectionbody">
<div class="admonitionblock warning">
<table>
<tr>
<td class="icon">
<div class="title">Warning</div>
</td>
<td class="content">
snippet not found: