Merge pull request #354 from Gerrit Meier
* gh-323: Polish “Provide a block macro for including an operation's snippets” Provide a block macro for including an operation's snippets
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -4,4 +4,5 @@ dependencies {
|
||||
compileOnly 'org.asciidoctor:asciidoctorj'
|
||||
testCompile 'junit:junit'
|
||||
testCompile 'org.asciidoctor:asciidoctorj'
|
||||
}
|
||||
testCompile 'org.springframework:spring-core'
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<div class="sect1">
|
||||
<h2 id="_curl_request">Curl request</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="listingblock">
|
||||
<div class="content">
|
||||
<pre class="highlight"><code class="language-bash" data-lang="bash">$ curl 'http://localhost:8080/' -i</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sect1">
|
||||
<h2 id="_custom_snippet">Custom snippet</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="listingblock">
|
||||
<div class="content">
|
||||
<pre class="highlight nowrap"><code class="language-http" data-lang="http">mycustomsnippet</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sect1">
|
||||
<h2 id="_http_request">HTTP request</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="listingblock">
|
||||
<div class="content">
|
||||
<pre class="highlight nowrap"><code class="language-http" data-lang="http">GET / HTTP/1.1
|
||||
Host: localhost:8080</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="paragraph">
|
||||
<p>No snippets found for operation::non-existent-operation</p>
|
||||
</div>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,21 @@
|
||||
<div class="sect1">
|
||||
<h2 id="_curl_request">Curl request</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="listingblock">
|
||||
<div class="content">
|
||||
<pre class="highlight"><code class="language-bash" data-lang="bash">$ curl 'http://localhost:8080/' -i</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sect1">
|
||||
<h2 id="_http_request">HTTP request</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="listingblock">
|
||||
<div class="content">
|
||||
<pre class="highlight nowrap"><code class="language-http" data-lang="http">GET / HTTP/1.1
|
||||
Host: localhost:8080</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<h2 id="_custom_snippet">Custom snippet</h2>
|
||||
@@ -0,0 +1,13 @@
|
||||
<div class="sect1">
|
||||
<h2 id="_section">Section</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="sect2">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="sect1">
|
||||
<h2 id="_curl_request">Curl request</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="listingblock">
|
||||
<div class="content">
|
||||
<pre class="highlight"><code class="language-bash" data-lang="bash">$ curl 'http://localhost:8080/' -i</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="sect2">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
[source,bash]
|
||||
----
|
||||
$ curl 'http://localhost:8080/' -i
|
||||
----
|
||||
@@ -0,0 +1,4 @@
|
||||
[source,http,options="nowrap"]
|
||||
----
|
||||
mycustomsnippet
|
||||
----
|
||||
@@ -0,0 +1,6 @@
|
||||
[source,http,options="nowrap"]
|
||||
----
|
||||
GET / HTTP/1.1
|
||||
Host: localhost:8080
|
||||
|
||||
----
|
||||
Reference in New Issue
Block a user