Add AsciidoctorJ 1.6 and 2.0 compatibility

AsciidoctorJ 1.6 is not backwards compatible with AsciidoctorJ 1.5
and AsciidoctorJ 2.0 is not backwards compatible with AsciidoctorJ 1.6
or 1.5. The backwards incompatibilities are such that they cannot be
worked around using reflection so code needs to be compiled against
each of the three versions. To this end, this commit splits
spring-restdocs-asciidoctor into several separate modules:

1. spring-restdocs-asciidoctor-support
2. spring-restdocs-asciidoctor-1.5
3. spring-restdocs-asciidoctor-1.6
4. spring-restdocs-asciidoctor-2.0

spring-restdocs-asciidoctor-support contains support code that is not
tied to a specific version of AsciidoctorJ and can be used with
1.5, 1.6 and 2.0. The other three modules contain code that is
specific to a particular version of AsciidoctorJ. Each
version-specific module uses class names that are unique across all
three modules and is written in such a way that they will back off
when used in an environment with a different version of AsciidoctorJ.
The existing spring-restdocs-asciidoctor module has been modified to
merge the version specific jars and the support jar together into a
single jar that supports AsciidoctorJ 1.5, 1.6, and 2.0.

The above-described changes should allow users to depend upon
spring-restdocs-asciidoctor as before and to now be able to use
AsciidoctorJ 1.5, 1.6, or 2.0.

Closes gh-581
This commit is contained in:
Andy Wilkinson
2019-06-18 12:03:42 +01:00
parent 00da0834df
commit ff18f8034d
23 changed files with 427 additions and 69 deletions

View File

@@ -1,40 +0,0 @@
/*
* Copyright 2014-2016 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
*
* https://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 org.asciidoctor.ast.Document;
import org.asciidoctor.extension.Preprocessor;
import org.asciidoctor.extension.PreprocessorReader;
/**
* {@link Preprocessor} that sets defaults for REST Docs-related {@link Document}
* attributes.
*
* @author Andy Wilkinson
*/
final class DefaultAttributesPreprocessor extends Preprocessor {
private final SnippetsDirectoryResolver snippetsDirectoryResolver = new SnippetsDirectoryResolver();
@Override
public PreprocessorReader process(Document document, PreprocessorReader reader) {
document.setAttr("snippets", this.snippetsDirectoryResolver
.getSnippetsDirectory(document.getAttributes()), false);
return reader;
}
}

View File

@@ -1,40 +0,0 @@
/*
* Copyright 2014-2016 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
*
* https://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 org.asciidoctor.Asciidoctor;
import org.asciidoctor.extension.spi.ExtensionRegistry;
/**
* Asciidoctor {@link ExtensionRegistry} for Spring REST Docs.
*
* @author Andy Wilkinson
*/
public final class RestDocsExtensionRegistry implements ExtensionRegistry {
@Override
public void register(Asciidoctor asciidoctor) {
asciidoctor.javaExtensionRegistry()
.preprocessor(new DefaultAttributesPreprocessor());
asciidoctor.rubyExtensionRegistry()
.loadClass(RestDocsExtensionRegistry.class
.getResourceAsStream("/extensions/operation_block_macro.rb"))
.blockMacro("operation", "OperationBlockMacro");
}
}

View File

@@ -1,83 +0,0 @@
/*
* Copyright 2014-2016 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
*
* https://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.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.function.Supplier;
/**
* Resolves the directory from which snippets can be read for inclusion in an Asciidoctor
* document. The resolved directory is relative to the {@code docdir} of the Asciidoctor
* document that it being rendered.
*
* @author Andy Wilkinson
*/
class SnippetsDirectoryResolver {
File getSnippetsDirectory(Map<String, Object> attributes) {
if (System.getProperty("maven.home") != null) {
return getMavenSnippetsDirectory(attributes);
}
return getGradleSnippetsDirectory(attributes);
}
private File getMavenSnippetsDirectory(Map<String, Object> attributes) {
Path docdir = Paths.get(getRequiredAttribute(attributes, "docdir"));
return new File(docdir.relativize(findPom(docdir).getParent()).toFile(),
"target/generated-snippets");
}
private Path findPom(Path docdir) {
Path path = docdir;
while (path != null) {
Path pom = path.resolve("pom.xml");
if (Files.isRegularFile(pom)) {
return pom;
}
path = path.getParent();
}
throw new IllegalStateException("pom.xml not found in '" + docdir + "' or above");
}
private File getGradleSnippetsDirectory(Map<String, Object> attributes) {
return new File(
getRequiredAttribute(attributes, "gradle-projectdir",
() -> getRequiredAttribute(attributes, "projectdir")),
"build/generated-snippets");
}
private String getRequiredAttribute(Map<String, Object> attributes, String name) {
return getRequiredAttribute(attributes, name, null);
}
private String getRequiredAttribute(Map<String, Object> attributes, String name,
Supplier<String> fallback) {
String attribute = (String) attributes.get(name);
if (attribute == null || attribute.length() == 0) {
if (fallback != null) {
return fallback.get();
}
throw new IllegalStateException(name + " attribute not found");
}
return attribute;
}
}

View File

@@ -1,20 +0,0 @@
/*
* Copyright 2014-2016 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
*
* https://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.
*/
/**
* Spring REST Docs Asciidoctor extensions.
*/
package org.springframework.restdocs.asciidoctor;

View File

@@ -1 +1,2 @@
org.springframework.restdocs.asciidoctor.RestDocsExtensionRegistry
org.springframework.restdocs.asciidoctor.RestDocsAsciidoctorJ15ExtensionRegistry
org.springframework.restdocs.asciidoctor.RestDocsAsciidoctorJ16ExtensionRegistry

View File

@@ -0,0 +1 @@
org.springframework.restdocs.asciidoctor.RestDocsAsciidoctorJ20ExtensionRegistry

View File

@@ -1,145 +0,0 @@
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', ''
operation = parent.sub_attributes operation
snippet_titles = SnippetTitles.new parent.document.attributes
content = read_snippets(snippets_dir, snippet_names, parent, operation,
snippet_titles)
add_blocks(content, parent.document, parent) unless content.empty?
nil
end
def read_snippets(snippets_dir, snippet_names, parent, operation,
snippet_titles)
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, parent, operation, snippet_titles)
end
end
def do_read_snippets(snippets, parent, operation, snippet_titles)
content = StringIO.new
section_level = parent.level + 1
section_id = parent.id
snippets.each do |snippet|
append_snippet_block(content, snippet, section_level, section_id,
operation, snippet_titles)
end
content.string
end
def add_blocks(content, doc, parent)
options = { safe: doc.options[:safe],
attributes: { 'projectdir' => doc.attr(:projectdir) } }
fragment = Asciidoctor.load content, options
fragment.blocks.each do |b|
b.parent = parent
parent << b
end
parent.find_by.each do |b|
b.parent = b.parent unless b.is_a? Asciidoctor::Document
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, section_id,
operation, snippet_titles)
write_title content, snippet, section_level, section_id, snippet_titles
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, id, snippet_titles)
section_level = '=' * (level + 1)
title = snippet_titles.title_for_snippet snippet
content.puts "[[#{id}_#{snippet.name.sub '-', '_'}]]"
content.puts "#{section_level} #{title}"
content.puts ''
end
# Details of a snippet to be rendered
class Snippet
attr_reader :name, :path
def initialize(path, name)
@path = path
@name = name
@snippet_titles
end
end
class SnippetTitles
@defaults = { '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 :defaults
end
def initialize(document_attributes)
@document_attributes = document_attributes
end
def title_for_snippet(snippet)
attribute_name = "operation-#{snippet.name}-title"
@document_attributes.fetch attribute_name do
SnippetTitles.defaults.fetch snippet.name, snippet.name.sub('-', ' ').capitalize
end
end
end
end

View File

@@ -1,61 +0,0 @@
/*
* Copyright 2014-2016 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
*
* https://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 org.asciidoctor.Asciidoctor;
import org.asciidoctor.Attributes;
import org.asciidoctor.Options;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DefaultAttributesPreprocessor}.
*
* @author Andy Wilkinson
*/
public class DefaultAttributesPreprocessorTests {
@Test
public void snippetsAttributeIsSet() {
Options options = new Options();
options.setAttributes(new Attributes("projectdir=../../.."));
String converted = Asciidoctor.Factory.create().convert("{snippets}", options);
assertThat(converted)
.contains("build" + File.separatorChar + "generated-snippets");
}
@Test
public void snippetsAttributeFromConvertArgumentIsNotOverridden() {
Options options = new Options();
options.setAttributes(new Attributes("snippets=custom projectdir=../../.."));
String converted = Asciidoctor.Factory.create().convert("{snippets}", options);
assertThat(converted).contains("custom");
}
@Test
public void snippetsAttributeFromDocumentPreambleIsNotOverridden() {
Options options = new Options();
options.setAttributes(new Attributes("projectdir=../../.."));
String converted = Asciidoctor.Factory.create()
.convert(":snippets: custom\n{snippets}", options);
assertThat(converted).contains("custom");
}
}

View File

@@ -1,133 +0,0 @@
/*
* Copyright 2014-2018 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
*
* https://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.util.HashMap;
import java.util.Map;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.equalTo;
/**
* Tests for {@link SnippetsDirectoryResolver}.
*
* @author Andy Wilkinson
*/
public class SnippetsDirectoryResolverTests {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void mavenProjectsUseTargetGeneratedSnippetsRelativeToDocdir()
throws IOException {
this.temporaryFolder.newFile("pom.xml");
Map<String, Object> attributes = new HashMap<>();
attributes.put("docdir",
new File(this.temporaryFolder.getRoot(), "src/main/asciidoc")
.getAbsolutePath());
File snippetsDirectory = getMavenSnippetsDirectory(attributes);
assertThat(snippetsDirectory).isRelative();
assertThat(snippetsDirectory)
.isEqualTo(new File("../../../target/generated-snippets"));
}
@Test
public void illegalStateExceptionWhenMavenPomCannotBeFound() throws IOException {
Map<String, Object> attributes = new HashMap<>();
String docdir = new File(this.temporaryFolder.getRoot(), "src/main/asciidoc")
.getAbsolutePath();
attributes.put("docdir", docdir);
this.thrown.expect(IllegalStateException.class);
this.thrown
.expectMessage(equalTo("pom.xml not found in '" + docdir + "' or above"));
getMavenSnippetsDirectory(attributes);
}
@Test
public void illegalStateWhenDocdirAttributeIsNotSetInMavenProject()
throws IOException {
Map<String, Object> attributes = new HashMap<>();
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage(equalTo("docdir attribute not found"));
getMavenSnippetsDirectory(attributes);
}
@Test
public void gradleProjectsUseBuildGeneratedSnippetsBeneathGradleProjectdir()
throws IOException {
Map<String, Object> attributes = new HashMap<>();
attributes.put("gradle-projectdir", "project/dir");
File snippetsDirectory = new SnippetsDirectoryResolver()
.getSnippetsDirectory(attributes);
assertThat(snippetsDirectory)
.isEqualTo(new File("project/dir/build/generated-snippets"));
}
@Test
public void gradleProjectsUseBuildGeneratedSnippetsBeneathGradleProjectdirWhenBothItAndProjectdirAreSet()
throws IOException {
Map<String, Object> attributes = new HashMap<>();
attributes.put("gradle-projectdir", "project/dir");
attributes.put("projectdir", "fallback/dir");
File snippetsDirectory = new SnippetsDirectoryResolver()
.getSnippetsDirectory(attributes);
assertThat(snippetsDirectory)
.isEqualTo(new File("project/dir/build/generated-snippets"));
}
@Test
public void gradleProjectsUseBuildGeneratedSnippetsBeneathProjectdirWhenGradleProjectdirIsNotSet()
throws IOException {
Map<String, Object> attributes = new HashMap<>();
attributes.put("projectdir", "project/dir");
File snippetsDirectory = new SnippetsDirectoryResolver()
.getSnippetsDirectory(attributes);
assertThat(snippetsDirectory)
.isEqualTo(new File("project/dir/build/generated-snippets"));
}
@Test
public void illegalStateWhenGradleProjectdirAndProjectdirAttributesAreNotSetInGradleProject()
throws IOException {
Map<String, Object> attributes = new HashMap<>();
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage(equalTo("projectdir attribute not found"));
new SnippetsDirectoryResolver().getSnippetsDirectory(attributes);
}
private File getMavenSnippetsDirectory(Map<String, Object> attributes) {
System.setProperty("maven.home", "/maven/home");
try {
return new SnippetsDirectoryResolver().getSnippetsDirectory(attributes);
}
finally {
System.clearProperty("maven.home");
}
}
}