Project documentation infrastructure

This commit adds support for an `HelpDocument` that can be generated
alongside the project. Such document can hold an arbitrary number of
sections with pre-defined sections such as "Getting Started" and "Next
Steps".

A default contributor retrieves the links for requested dependencies
and add them to the document.

Closes gh-353

Co-authored-by: Madhura Bhave <mbhave@pivotal.io>
This commit is contained in:
Stephane Nicoll 2019-02-18 11:33:33 +01:00 committed by Madhura Bhave
parent 2746a3a6e7
commit 924a73310a
26 changed files with 1384 additions and 4 deletions

View File

@ -19,6 +19,11 @@
<artifactId>initializr-metadata</artifactId>
</dependency>
<dependency>
<groupId>com.samskivert</groupId>
<artifactId>jmustache</artifactId>
</dependency>
<dependency>
<groupId>io.spring.initializr</groupId>
<artifactId>initializr-generator</artifactId>

View File

@ -0,0 +1,116 @@
/*
* Copyright 2012-2019 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 io.spring.initializr.generator.spring.documentation;
import java.util.ArrayList;
import java.util.List;
import io.spring.initializr.generator.io.template.MustacheTemplateRenderer;
import io.spring.initializr.generator.io.text.BulletedSection;
import io.spring.initializr.generator.io.text.Section;
/**
* Section that provides links and other important references to get started.
*
* @author Madhura Bhave
* @author Stephane Nicoll
*/
public final class GettingStartedSection extends PreDefinedSection {
private final BulletedSection<Link> referenceDocs;
private final BulletedSection<Link> guides;
private final BulletedSection<Link> additionalLinks;
GettingStartedSection(MustacheTemplateRenderer templateRenderer) {
super("Getting Started");
this.referenceDocs = new BulletedSection<>(templateRenderer,
"documentation/reference-documentation");
this.guides = new BulletedSection<>(templateRenderer, "documentation/guides");
this.additionalLinks = new BulletedSection<>(templateRenderer,
"documentation/additional-links");
}
@Override
public boolean isEmpty() {
return referenceDocs().isEmpty() && guides().isEmpty()
&& additionalLinks().isEmpty() && super.isEmpty();
}
@Override
protected List<Section> resolveSubSections(List<Section> sections) {
List<Section> allSections = new ArrayList<>();
allSections.add(this.referenceDocs);
allSections.add(this.guides);
allSections.add(this.additionalLinks);
allSections.addAll(sections);
return allSections;
}
public GettingStartedSection addReferenceDocLink(String href, String description) {
this.referenceDocs.addItem(new Link(href, description));
return this;
}
public BulletedSection<Link> referenceDocs() {
return this.referenceDocs;
}
public GettingStartedSection addGuideLink(String href, String description) {
this.guides.addItem(new Link(href, description));
return this;
}
public BulletedSection<Link> guides() {
return this.guides;
}
public GettingStartedSection addAdditionalLink(String href, String description) {
this.additionalLinks.addItem(new Link(href, description));
return this;
}
public BulletedSection<Link> additionalLinks() {
return this.additionalLinks;
}
/**
* Internal representation of a link.
*/
public static class Link {
private final String href;
private final String description;
Link(String href, String description) {
this.href = href;
this.description = description;
}
public String getHref() {
return this.href;
}
public String getDescription() {
return this.description;
}
}
}

View File

@ -0,0 +1,95 @@
/*
* Copyright 2012-2019 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 io.spring.initializr.generator.spring.documentation;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import io.spring.initializr.generator.io.template.MustacheTemplateRenderer;
import io.spring.initializr.generator.io.text.MustacheSection;
import io.spring.initializr.generator.io.text.Section;
/**
* Project's help document intended to give additional references to the users. Contains a
* getting started section, additional sections and a next steps section.
*
* @author Stephane Nicoll
* @author Madhura Bhave
*/
public class HelpDocument {
private final MustacheTemplateRenderer templateRenderer;
private final GettingStartedSection gettingStarted;
private final PreDefinedSection nextSteps;
private final LinkedList<Section> sections = new LinkedList<>();
public HelpDocument(MustacheTemplateRenderer templateRenderer) {
this.templateRenderer = templateRenderer;
this.gettingStarted = new GettingStartedSection(templateRenderer);
this.nextSteps = new PreDefinedSection("Next Steps");
}
public GettingStartedSection gettingStarted() {
return this.gettingStarted;
}
public PreDefinedSection nextSteps() {
return this.nextSteps;
}
public HelpDocument addSection(Section section) {
this.sections.add(section);
return this;
}
/**
* Add a section rendered by the specified mustache template and model.
* @param templateName the name of the mustache template to render
* @param model the model that should be used for the rendering
* @return this document
*/
public HelpDocument addSection(String templateName, Map<String, Object> model) {
return addSection(
new MustacheSection(this.templateRenderer, templateName, model));
}
public List<Section> getSections() {
return Collections.unmodifiableList(this.sections);
}
public void write(PrintWriter writer) throws IOException {
LinkedList<Section> allSections = new LinkedList<>(this.sections);
allSections.addFirst(this.gettingStarted);
allSections.addLast(this.nextSteps);
for (Section section : allSections) {
section.write(writer);
}
}
public boolean isEmpty() {
return gettingStarted().isEmpty() && this.sections.isEmpty()
&& nextSteps().isEmpty();
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2012-2019 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 io.spring.initializr.generator.spring.documentation;
import org.springframework.core.Ordered;
/**
* Callback for customizing a project's {@link HelpDocument}. Invoked with an
* {@link Ordered order} of {@code 0} by default, considering overriding
* {@link #getOrder()} to customize this behaviour.
*
* @author Stephane Nicoll
*/
@FunctionalInterface
public interface HelpDocumentCustomizer extends Ordered {
void customize(HelpDocument document);
@Override
default int getOrder() {
return 0;
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright 2012-2019 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 io.spring.initializr.generator.spring.documentation;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import io.spring.initializr.generator.project.contributor.ProjectContributor;
/**
* {@link ProjectContributor} for the project's {@code HELP.md} file.
*
* @author Stephane Nicoll
* @author Madhura Bhave
*/
public class HelpDocumentProjectContributor implements ProjectContributor {
private final HelpDocument helpDocument;
public HelpDocumentProjectContributor(HelpDocument helpDocument) {
this.helpDocument = helpDocument;
}
@Override
public void contribute(Path projectRoot) throws IOException {
if (this.helpDocument.isEmpty()) {
return;
}
Path file = Files.createFile(projectRoot.resolve("HELP.md"));
try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(file))) {
this.helpDocument.write(writer);
}
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2012-2019 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 io.spring.initializr.generator.spring.documentation;
import io.spring.initializr.generator.io.template.MustacheTemplateRenderer;
import io.spring.initializr.generator.project.ProjectGenerationConfiguration;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
/**
* Configuration for contributions specific to the help documentation of a project.
*
* @author Stephane Nicoll
*/
@ProjectGenerationConfiguration
@Import(HelpDocumentProjectGenerationDefaultContributorsConfiguration.class)
public class HelpDocumentProjectGenerationConfiguration {
@Bean
public HelpDocumentProjectContributor helpDocumentProjectContributor(
MustacheTemplateRenderer templateRenderer,
ObjectProvider<HelpDocumentCustomizer> helpDocumentCustomizers) {
HelpDocument helpDocument = new HelpDocument(templateRenderer);
helpDocumentCustomizers.orderedStream()
.forEach((customizer) -> customizer.customize(helpDocument));
return new HelpDocumentProjectContributor(helpDocument);
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2012-2019 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 io.spring.initializr.generator.spring.documentation;
import io.spring.initializr.generator.project.ResolvedProjectDescription;
import io.spring.initializr.metadata.InitializrMetadata;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Default {@link HelpDocument} contributors.
*
* @author Stephane Nicoll
*/
@Configuration
public class HelpDocumentProjectGenerationDefaultContributorsConfiguration {
@Bean
public RequestedDependenciesHelpDocumentCustomizer dependenciesHelpDocumentCustomizer(
ResolvedProjectDescription description, InitializrMetadata metadata) {
return new RequestedDependenciesHelpDocumentCustomizer(description, metadata);
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright 2012-2019 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 io.spring.initializr.generator.spring.documentation;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import io.spring.initializr.generator.io.text.Section;
/**
* Section that is pre-defined and always present in the document. You can only add
* additional sections to pre-defined sections.
*
* @author Madhura Bhave
*/
public class PreDefinedSection implements Section {
private final String title;
private final List<Section> subSections = new ArrayList<>();
public PreDefinedSection(String title) {
this.title = title;
}
public PreDefinedSection addSection(Section section) {
this.subSections.add(section);
return this;
}
@Override
public void write(PrintWriter writer) throws IOException {
if (!isEmpty()) {
writer.println("# " + this.title);
writer.println("");
for (Section section : resolveSubSections(this.subSections)) {
section.write(writer);
}
}
}
public boolean isEmpty() {
return this.subSections.isEmpty();
}
/**
* Resolve the sections to render based on the current registered sections.
* @param sections the registered sections
* @return the sections to render
*/
protected List<Section> resolveSubSections(List<Section> sections) {
return sections;
}
}

View File

@ -0,0 +1,78 @@
/*
* Copyright 2012-2019 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 io.spring.initializr.generator.spring.documentation;
import io.spring.initializr.generator.project.ResolvedProjectDescription;
import io.spring.initializr.metadata.Dependency;
import io.spring.initializr.metadata.InitializrMetadata;
import org.springframework.core.Ordered;
/**
* A {@link HelpDocumentCustomizer} that register links for selected dependencies.
*
* @author Stephane Nicoll
*/
public class RequestedDependenciesHelpDocumentCustomizer
implements HelpDocumentCustomizer {
private final ResolvedProjectDescription projectDescription;
private final InitializrMetadata metadata;
public RequestedDependenciesHelpDocumentCustomizer(
ResolvedProjectDescription projectDescription, InitializrMetadata metadata) {
this.projectDescription = projectDescription;
this.metadata = metadata;
}
@Override
public void customize(HelpDocument document) {
this.projectDescription.getRequestedDependencies().forEach((id, dependency) -> {
Dependency dependencyMetadata = this.metadata.getDependencies().get(id);
if (dependencyMetadata != null) {
handleDependency(document, dependencyMetadata);
}
});
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
private void handleDependency(HelpDocument document, Dependency dependency) {
GettingStartedSection gettingStartedSection = document.gettingStarted();
dependency.getLinks().forEach((link) -> {
if (link.getDescription() != null && link.getRel() != null) {
if ("reference".equals(link.getRel())) {
gettingStartedSection.addReferenceDocLink(link.getHref(),
link.getDescription());
}
else if ("guide".equals(link.getRel())) {
gettingStartedSection.addGuideLink(link.getHref(),
link.getDescription());
}
else {
gettingStartedSection.addAdditionalLink(link.getHref(),
link.getDescription());
}
}
});
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright 2012-2019 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.
*/
/**
* Documentation contributors. Generate a {@code HELP.md} at the root of the project with
* additional information based on requested dependencies.
*/
package io.spring.initializr.generator.spring.documentation;

View File

@ -7,4 +7,5 @@ io.spring.initializr.generator.spring.code.groovy.GroovyProjectGenerationConfigu
io.spring.initializr.generator.spring.code.java.JavaProjectGenerationConfiguration,\
io.spring.initializr.generator.spring.code.kotlin.KotlinProjectGenerationConfiguration,\
io.spring.initializr.generator.spring.configuration.ApplicationConfigurationProjectGenerationConfiguration,\
io.spring.initializr.generator.spring.documentation.HelpDocumentProjectGenerationConfiguration,\
io.spring.initializr.generator.spring.scm.git.GitProjectGenerationConfiguration

View File

@ -0,0 +1,6 @@
### Additional Links
These additional references should also help you:
{{#items}}
* [{{description}}]({{href}})
{{/items}}

View File

@ -0,0 +1,6 @@
### Guides
The following guides illustrates how to use certain features concretely:
{{#items}}
* [{{description}}]({{href}})
{{/items}}

View File

@ -0,0 +1,6 @@
### Reference Documentation
For further reference, please consider the following sections:
{{#items}}
* [{{description}}]({{href}})
{{/items}}

View File

@ -0,0 +1,71 @@
/*
* Copyright 2012-2019 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 io.spring.initializr.generator.spring.documentation;
import io.spring.initializr.generator.io.template.MustacheTemplateRenderer;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link GettingStartedSection}.
*
* @author Stephane Nicoll
*/
class GettingStartedSectionTests {
private final MustacheTemplateRenderer renderer = new MustacheTemplateRenderer("");
@Test
void gettingStartedEmpty() {
GettingStartedSection gettingStarted = newGettingStartedSection();
assertThat(gettingStarted.isEmpty()).isTrue();
}
@Test
void gettingStartedWithGuideLinkIsNotEmpty() {
GettingStartedSection gettingStarted = newGettingStartedSection();
gettingStarted.addGuideLink("https://example.com", "Test");
assertThat(gettingStarted.isEmpty()).isFalse();
}
@Test
void gettingStartedWithReferenceDocLinkIsNotEmpty() {
GettingStartedSection gettingStarted = newGettingStartedSection();
gettingStarted.addReferenceDocLink("https://example.com", "Test");
assertThat(gettingStarted.isEmpty()).isFalse();
}
@Test
void gettingStartedWithAdditionalLinkIsNotEmpty() {
GettingStartedSection gettingStarted = newGettingStartedSection();
gettingStarted.addAdditionalLink("https://example.com", "Test");
assertThat(gettingStarted.isEmpty()).isFalse();
}
@Test
void gettingStartedWithSubSectionIsNotEmpty() {
GettingStartedSection gettingStarted = newGettingStartedSection();
gettingStarted.addSection((writer) -> writer.println("test"));
assertThat(gettingStarted.isEmpty()).isFalse();
}
private GettingStartedSection newGettingStartedSection() {
return new GettingStartedSection(this.renderer);
}
}

View File

@ -0,0 +1,124 @@
/*
* Copyright 2012-2019 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 io.spring.initializr.generator.spring.documentation;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import io.spring.initializr.generator.io.template.MustacheTemplateRenderer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link HelpDocumentProjectContributor}.
*
* @author Stephane Nicoll
*/
class HelpDocumentProjectContributorTests {
private Path directory;
private MustacheTemplateRenderer templateRenderer;
@BeforeEach
void setup(@TempDir Path directory) {
this.directory = directory;
this.templateRenderer = new MustacheTemplateRenderer("classpath:/templates");
}
@Test
void helpDocumentEmptyDoesNotCreateFile() throws IOException {
HelpDocument document = new HelpDocument(this.templateRenderer);
assertThat(document.isEmpty()).isTrue();
Path projectDir = Files.createTempDirectory(this.directory, "project-");
new HelpDocumentProjectContributor(document).contribute(projectDir);
Path helpDocument = projectDir.resolve("HELP.md");
assertThat(helpDocument).doesNotExist();
}
@Test
void helpDocumentWithLinksToGuide() throws IOException {
HelpDocument document = new HelpDocument(this.templateRenderer);
document.gettingStarted().addGuideLink("https://test.example.com", "test")
.addGuideLink("https://test2.example.com", "test2");
List<String> lines = generateDocument(document);
assertThat(lines).containsExactly("# Getting Started", "", "### Guides",
"The following guides illustrates how to use certain features concretely:",
"", "* [test](https://test.example.com)",
"* [test2](https://test2.example.com)", "");
}
@Test
void helpDocumentWithLinksToReferenceDoc() throws IOException {
HelpDocument document = new HelpDocument(this.templateRenderer);
document.gettingStarted().addReferenceDocLink("https://test.example.com", "doc")
.addReferenceDocLink("https://test2.example.com", "doc2");
List<String> lines = generateDocument(document);
assertThat(lines).containsExactly("# Getting Started", "",
"### Reference Documentation",
"For further reference, please consider the following sections:", "",
"* [doc](https://test.example.com)",
"* [doc2](https://test2.example.com)", "");
}
@Test
void helpDocumentWithLinksToOtherLinks() throws IOException {
HelpDocument document = new HelpDocument(this.templateRenderer);
document.gettingStarted().addAdditionalLink("https://test.example.com",
"Something");
List<String> lines = generateDocument(document);
assertThat(lines).containsExactly("# Getting Started", "", "### Additional Links",
"These additional references should also help you:", "",
"* [Something](https://test.example.com)", "");
}
@Test
void helpDocumentWithSimpleSection() throws IOException {
HelpDocument document = new HelpDocument(this.templateRenderer);
document.addSection((writer) -> writer
.println(String.format("# My test section%n%n * Test")));
List<String> lines = generateDocument(document);
assertThat(lines).containsExactly("# My test section", "", " * Test");
}
@Test
void helpDocumentWithLinksAndSimpleSection() throws IOException {
HelpDocument document = new HelpDocument(this.templateRenderer);
document.gettingStarted().addGuideLink("https://test.example.com", "test")
.addSection((writer) -> writer
.println(String.format("# My test section%n%n * Test")));
List<String> lines = generateDocument(document);
assertThat(lines).containsExactly("# Getting Started", "", "### Guides",
"The following guides illustrates how to use certain features concretely:",
"", "* [test](https://test.example.com)", "", "# My test section", "",
" * Test");
}
private List<String> generateDocument(HelpDocument document) throws IOException {
Path projectDir = Files.createTempDirectory(this.directory, "project-");
new HelpDocumentProjectContributor(document).contribute(projectDir);
Path helpDocument = projectDir.resolve("HELP.md");
assertThat(helpDocument).isRegularFile();
return Files.readAllLines(helpDocument);
}
}

View File

@ -0,0 +1,77 @@
/*
* Copyright 2012-2019 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 io.spring.initializr.generator.spring.documentation;
import java.nio.file.Path;
import io.spring.initializr.generator.io.template.MustacheTemplateRenderer;
import io.spring.initializr.generator.project.ProjectDescription;
import io.spring.initializr.generator.spring.test.InitializrMetadataTestBuilder;
import io.spring.initializr.generator.test.project.ProjectAssetTester;
import io.spring.initializr.metadata.Dependency;
import io.spring.initializr.metadata.InitializrMetadata;
import io.spring.initializr.metadata.Link;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link HelpDocumentProjectGenerationConfiguration}.
*
* @author Stephane Nicoll
*/
class HelpDocumentProjectGenerationConfigurationTests {
private ProjectAssetTester projectTester;
private InitializrMetadataTestBuilder metadataBuilder = InitializrMetadataTestBuilder
.withDefaults();
@BeforeEach
void setup(@TempDir Path directory) {
this.projectTester = new ProjectAssetTester()
.withConfiguration(HelpDocumentProjectGenerationConfiguration.class)
.withBean(MustacheTemplateRenderer.class,
() -> new MustacheTemplateRenderer("classpath:/templates"))
.withBean(InitializrMetadata.class, () -> this.metadataBuilder.build())
.withDirectory(directory);
}
@Test
void helpDocumentIsNotContributedWithoutLinks() {
assertThat(this.projectTester.generate(new ProjectDescription())
.getRelativePathsOfProjectFiles()).isEmpty();
}
@Test
void helpDocumentIsContributedWithLinks() {
Dependency dependency = Dependency.withId("example", "com.example", "example");
dependency.getLinks().add(
Link.create("guide", "https://example.com/how-to", "How-to example"));
dependency.getLinks().add(Link.create("reference", "https://example.com/doc",
"Reference doc example"));
this.metadataBuilder.addDependencyGroup("test", dependency);
ProjectDescription description = new ProjectDescription();
description.addDependency("example", null);
assertThat(
this.projectTester.generate(description).getRelativePathsOfProjectFiles())
.containsOnly("HELP.md");
}
}

View File

@ -0,0 +1,93 @@
/*
* Copyright 2012-2019 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 io.spring.initializr.generator.spring.documentation;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import io.spring.initializr.generator.io.template.MustacheTemplateRenderer;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyZeroInteractions;
/**
* Tests for {@link HelpDocument}.
*
* @author Stephane Nicoll
*/
class HelpDocumentTests {
private final MustacheTemplateRenderer templateRenderer = new MustacheTemplateRenderer(
"classpath:/templates");
@Test
void renderEmptyDocumentDoesNotCallWriter() throws IOException {
HelpDocument document = new HelpDocument(this.templateRenderer);
PrintWriter out = mock(PrintWriter.class);
document.write(out);
verifyZeroInteractions(out);
}
@Test
void renderSingleSection() {
HelpDocument document = new HelpDocument(this.templateRenderer);
document.addSection((writer) -> writer.println("# Test"));
String out = write(document);
assertThat(out).contains("# Test", "");
}
@Test
void renderLinks() {
HelpDocument document = new HelpDocument(this.templateRenderer);
document.gettingStarted().addReferenceDocLink("https://example.com/doc", "Doc");
document.gettingStarted().addGuideLink("https://example.com/guide-1", "Guide 1");
document.gettingStarted().addGuideLink("https://example.com/guide-2", "Guide 2");
String out = write(document);
assertThat(out).contains("# Getting Started", "", "### Reference Documentation",
"For further reference, please consider the following sections:", "",
"* [Doc](https://example.com/doc)", "", "### Guides",
"The following guides illustrates how to use certain features concretely:",
"", "* [Guide 1](https://example.com/guide-1)",
"* [Guide 2](https://example.com/guide-2)", "");
}
@Test
void renderOnlyAdditionalLink() {
HelpDocument document = new HelpDocument(this.templateRenderer);
document.gettingStarted().addAdditionalLink("https://example.com/app",
"Test App");
String out = write(document);
assertThat(out).contains("# Getting Started", "", "### Additional Links",
"These additional references should also help you:", "",
"* [Test App](https://example.com/app)", "");
}
private String write(HelpDocument document) {
try {
StringWriter out = new StringWriter();
document.write(new PrintWriter(out));
return out.toString();
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
}

View File

@ -0,0 +1,113 @@
/*
* Copyright 2012-2019 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 io.spring.initializr.generator.spring.documentation;
import java.util.List;
import io.spring.initializr.generator.io.template.MustacheTemplateRenderer;
import io.spring.initializr.generator.project.ProjectDescription;
import io.spring.initializr.generator.project.ResolvedProjectDescription;
import io.spring.initializr.generator.spring.test.InitializrMetadataTestBuilder;
import io.spring.initializr.metadata.Dependency;
import io.spring.initializr.metadata.InitializrMetadata;
import io.spring.initializr.metadata.Link;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link RequestedDependenciesHelpDocumentCustomizer}.
*
* @author Stephane Nicoll
*/
class RequestedDependenciesHelpDocumentCustomizerTests {
private final InitializrMetadataTestBuilder metadataBuilder = InitializrMetadataTestBuilder
.withDefaults();
@Test
void dependencyLinkWithNoDescriptionIsIgnored() {
Dependency dependency = Dependency.withId("example", "com.example", "example");
dependency.getLinks().add(Link.create("guide", "https://example.com/how-to"));
this.metadataBuilder.addDependencyGroup("test", dependency);
HelpDocument document = customizeHelp("example");
assertThat(document.gettingStarted().isEmpty()).isTrue();
}
@Test
void dependencyWithReferenceDocLink() {
Dependency dependency = Dependency.withId("example", "com.example", "example");
dependency.getLinks().add(Link.create("reference", "https://example.com/doc",
"Reference doc example"));
this.metadataBuilder.addDependencyGroup("test", dependency);
HelpDocument document = customizeHelp("example");
assertThat(document.gettingStarted().isEmpty()).isFalse();
List<GettingStartedSection.Link> links = document.gettingStarted().referenceDocs()
.getItems();
assertThat(links).hasSize(1);
assertLink(links.get(0), "https://example.com/doc", "Reference doc example");
}
@Test
void dependencyWithGuideLink() {
Dependency dependency = Dependency.withId("example", "com.example", "example");
dependency.getLinks().add(
Link.create("guide", "https://example.com/how-to", "How-to example"));
this.metadataBuilder.addDependencyGroup("test", dependency);
HelpDocument document = customizeHelp("example");
assertThat(document.gettingStarted().isEmpty()).isFalse();
List<GettingStartedSection.Link> links = document.gettingStarted().guides()
.getItems();
assertThat(links).hasSize(1);
assertLink(links.get(0), "https://example.com/how-to", "How-to example");
}
@Test
void dependencyWithAdditionalLink() {
Dependency dependency = Dependency.withId("example", "com.example", "example");
dependency.getLinks()
.add(Link.create("something", "https://example.com/test", "Test App"));
this.metadataBuilder.addDependencyGroup("test", dependency);
HelpDocument document = customizeHelp("example");
assertThat(document.gettingStarted().isEmpty()).isFalse();
List<GettingStartedSection.Link> links = document.gettingStarted()
.additionalLinks().getItems();
assertThat(links).hasSize(1);
assertLink(links.get(0), "https://example.com/test", "Test App");
}
private void assertLink(GettingStartedSection.Link link, String href,
String description) {
assertThat(link.getHref()).isEqualTo(href);
assertThat(link.getDescription()).isEqualTo(description);
}
private HelpDocument customizeHelp(String... requestedDependencies) {
ProjectDescription description = new ProjectDescription();
for (String requestedDependency : requestedDependencies) {
description.addDependency(requestedDependency, null);
}
InitializrMetadata metadata = this.metadataBuilder.build();
HelpDocument document = new HelpDocument(
new MustacheTemplateRenderer("classpath:/templates"));
new RequestedDependenciesHelpDocumentCustomizer(
new ResolvedProjectDescription(description), metadata)
.customize(document);
return document;
}
}

View File

@ -0,0 +1,78 @@
/*
* Copyright 2012-2019 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 io.spring.initializr.generator.io.text;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.spring.initializr.generator.io.template.TemplateRenderer;
/**
* {@link Section} for list of items using a {@link TemplateRenderer}.
*
* @param <T> the type of the item in the bullets
* @author Madhura Bhave
*/
public class BulletedSection<T> implements Section {
private final TemplateRenderer templateRenderer;
private final String templateName;
private final String itemName;
private List<T> items = new ArrayList<>();
public BulletedSection(TemplateRenderer templateRenderer, String templateName) {
this(templateRenderer, templateName, "items");
}
public BulletedSection(TemplateRenderer templateRenderer, String templateName,
String itemName) {
this.templateRenderer = templateRenderer;
this.templateName = templateName;
this.itemName = itemName;
}
public BulletedSection addItem(T item) {
this.items.add(item);
return this;
}
public boolean isEmpty() {
return this.items.isEmpty();
}
public List<T> getItems() {
return Collections.unmodifiableList(this.items);
}
@Override
public void write(PrintWriter writer) throws IOException {
if (!isEmpty()) {
Map<String, Object> model = new HashMap<>();
model.put(this.itemName, this.items);
writer.println(this.templateRenderer.render(this.templateName, model));
}
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright 2012-2019 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 io.spring.initializr.generator.io.text;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import io.spring.initializr.generator.io.template.MustacheTemplateRenderer;
/**
* {@link Section} that uses a {@link MustacheTemplateRenderer}. Renders the content with
* a newline at the end.
*
* @author Madhura Bhave
*/
public class MustacheSection implements Section {
private final MustacheTemplateRenderer templateRenderer;
private final String templateName;
private final Map<String, Object> model;
public MustacheSection(MustacheTemplateRenderer templateRenderer, String templateName,
Map<String, Object> model) {
this.templateRenderer = templateRenderer;
this.templateName = templateName;
this.model = model;
}
@Override
public void write(PrintWriter writer) throws IOException {
writer.println(this.templateRenderer.render(this.templateName,
resolveModel(this.model)));
}
/**
* Resolve the {@code model} prior to render the section.
* @param model the current model
* @return the model to use to render this section (never null)
*/
protected Map<String, Object> resolveModel(Map<String, Object> model) {
return model;
}
}

View File

@ -0,0 +1,101 @@
/*
* Copyright 2012-2019 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 io.spring.initializr.generator.io.text;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Collections;
import java.util.Map;
import io.spring.initializr.generator.io.template.TemplateRenderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests for {@link BulletedSection}.
*
* @author Stephane Nicoll
*/
@ExtendWith(MockitoExtension.class)
class BulletedSectionTests {
@Mock
private TemplateRenderer renderer;
@Captor
private ArgumentCaptor<Map<String, Object>> modelCaptor;
@Test
void bulletedSectionEmpty() {
assertThat(new BulletedSection<String>(this.renderer, "test").isEmpty()).isTrue();
}
@Test
void bulletedSectionEmptyDoesNotInvokeRender() throws IOException {
BulletedSection<String> section = new BulletedSection<>(this.renderer, "test");
PrintWriter writer = mock(PrintWriter.class);
section.write(writer);
verifyNoMoreInteractions(writer, this.renderer);
}
@Test
void bulletedSectionWithItem() {
BulletedSection<String> section = new BulletedSection<>(this.renderer, "test");
section.addItem("test");
assertThat(section.isEmpty()).isFalse();
}
@Test
void bulletedSectionWithDefaultItemName() throws IOException {
given(this.renderer.render(eq("template"), any())).willReturn("output");
BulletedSection<String> section = new BulletedSection<>(this.renderer,
"template");
section.addItem("test");
section.write(new PrintWriter(new StringWriter()));
verify(this.renderer).render(eq("template"), this.modelCaptor.capture());
Map<String, Object> model = this.modelCaptor.getValue();
assertThat(model).containsOnly(entry("items", Collections.singletonList("test")));
}
@Test
void bulletedSectionWithCustomItemName() throws IOException {
given(this.renderer.render(eq("template"), any())).willReturn("output");
BulletedSection<String> section = new BulletedSection<>(this.renderer, "template",
"elements");
section.addItem("test");
section.write(new PrintWriter(new StringWriter()));
verify(this.renderer).render(eq("template"), this.modelCaptor.capture());
Map<String, Object> model = this.modelCaptor.getValue();
assertThat(model)
.containsOnly(entry("elements", Collections.singletonList("test")));
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright 2012-2019 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 io.spring.initializr.generator.io.text;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Collections;
import java.util.Map;
import com.samskivert.mustache.MustacheException;
import io.spring.initializr.generator.io.template.MustacheTemplateRenderer;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Tests for {@link MustacheSection}.
*
* @author Stephane Nicoll
*/
class MustacheSectionTests {
private final MustacheTemplateRenderer renderer = new MustacheTemplateRenderer(
"classpath:/templates/mustache");
@Test
void renderSection() throws IOException {
MustacheSection section = new MustacheSection(this.renderer, "test",
Collections.singletonMap("key", "hello"));
StringWriter writer = new StringWriter();
section.write(new PrintWriter(writer));
assertThat(writer.toString()).isEqualTo(String.format("hello%n"));
}
@Test
void renderSectionWithMissingKey() {
MustacheSection section = new MustacheSection(this.renderer, "test",
Collections.singletonMap("another", "hello"));
assertThatThrownBy(() -> section.write(new PrintWriter(new StringWriter())))
.isInstanceOf(MustacheException.class).hasMessageContaining("key");
}
@Test
void renderSectionWithCustomModelResolution() throws IOException {
MustacheSection section = new MustacheSection(this.renderer, "test",
Collections.emptyMap()) {
@Override
protected Map<String, Object> resolveModel(Map<String, Object> model) {
return Collections.singletonMap("key", "custom");
}
};
StringWriter writer = new StringWriter();
section.write(new PrintWriter(writer));
assertThat(writer.toString()).isEqualTo(String.format("custom%n"));
}
}

View File

@ -17,13 +17,14 @@
package io.spring.initializr.generator.test.project;
import java.nio.file.Path;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;
import io.spring.initializr.generator.io.IndentingWriterFactory;
import io.spring.initializr.generator.io.SimpleIndentStrategy;
import io.spring.initializr.generator.io.template.MustacheTemplateRenderer;
import io.spring.initializr.generator.project.DefaultProjectAssetGenerator;
import io.spring.initializr.generator.project.ProjectAssetGenerator;
import io.spring.initializr.generator.project.ProjectDescription;
@ -49,8 +50,12 @@ public class ProjectGeneratorTester
}
private static Map<Class<?>, Supplier<?>> defaultBeans() {
return Collections.singletonMap(IndentingWriterFactory.class,
Map<Class<?>, Supplier<?>> beans = new HashMap<>();
beans.put(IndentingWriterFactory.class,
() -> IndentingWriterFactory.create(new SimpleIndentStrategy(" ")));
beans.put(MustacheTemplateRenderer.class,
() -> new MustacheTemplateRenderer("classpath:/templates"));
return beans;
}
@Override

View File

@ -89,8 +89,8 @@ public class InitializrAutoConfiguration {
}
@Bean
@ConditionalOnMissingBean
public TemplateRenderer templateRenderer(Environment environment,
@ConditionalOnMissingBean(TemplateRenderer.class)
public MustacheTemplateRenderer templateRenderer(Environment environment,
ObjectProvider<CacheManager> cacheManager) {
return new MustacheTemplateRenderer("classpath:/templates",
determineCache(environment, cacheManager.getIfAvailable()));

View File

@ -24,6 +24,7 @@ import java.util.Map;
import io.spring.initializr.generator.io.IndentingWriterFactory;
import io.spring.initializr.generator.io.SimpleIndentStrategy;
import io.spring.initializr.generator.io.template.MustacheTemplateRenderer;
import io.spring.initializr.generator.project.ProjectDirectoryFactory;
import io.spring.initializr.generator.spring.test.InitializrMetadataTestBuilder;
import io.spring.initializr.generator.spring.test.ProjectAssert;
@ -206,6 +207,11 @@ public class ProjectGenerationInvokerTests {
return IndentingWriterFactory.create(new SimpleIndentStrategy("\t"));
}
@Bean
public MustacheTemplateRenderer templateRenderer() {
return new MustacheTemplateRenderer("classpath:/templates");
}
@Bean
public ProjectDirectoryFactory projectDirectoryFactory() {
return (description) -> Files.createTempDirectory("project-");