Provide a block macro for including an operation's snippets

See gh-323
Closes gh-354
This commit is contained in:
Gerrit Meier
2017-02-25 23:57:27 +01:00
committed by Andy Wilkinson
parent 3cc6f2d7d5
commit 18b559a839
14 changed files with 365 additions and 2 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
<h2 id="_custom_snippet">Custom snippet</h2>

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
<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:

View File

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

View File

@@ -0,0 +1,4 @@
[source,bash]
----
$ curl 'http://localhost:8080/' -i
----

View File

@@ -0,0 +1,4 @@
[source,http,options="nowrap"]
----
mycustomsnippet
----

View File

@@ -0,0 +1,6 @@
[source,http,options="nowrap"]
----
GET / HTTP/1.1
Host: localhost:8080
----