From f1803348d632896b92f68a8093bf6492440369b0 Mon Sep 17 00:00:00 2001 From: Florian Benz Date: Fri, 23 Mar 2018 14:18:41 +0100 Subject: [PATCH] Initial Kotlin support (#210) * Initial Kotlin support * Simplify sentence --- pom.xml | 3 + .../FieldDocumentationObjectVisitor.java | 9 +- spring-auto-restdocs-dokka-json/.gitignore | 29 ++ spring-auto-restdocs-dokka-json/README.md | 42 +++ spring-auto-restdocs-dokka-json/pom.xml | 96 +++++++ .../scalable/dokka/json/JsonFileGenerator.kt | 85 ++++++ .../dokka/json/JsonFormatDescriptor.kt | 40 +++ .../scalable/dokka/json/JsonFormatService.kt | 131 +++++++++ .../format/auto-restdocs-json.properties | 2 + .../dokka/json/FileComparisonFailure.kt | 54 ++++ .../dokka/json/JsonFileGeneratorTest.kt | 78 ++++++ .../dokka/json/JsonFormatServiceTest.kt | 81 ++++++ .../capital/scalable/dokka/json/TestAPI.kt | 261 ++++++++++++++++++ .../testdata/JavaClass.NestedJavaClass.json | 5 + .../testdata/JavaClass.java | 36 +++ .../testdata/JavaClass.json | 26 ++ .../testdata/KotlinDataClass.NestedClass.json | 10 + .../testdata/KotlinDataClass.json | 32 +++ .../testdata/KotlinDataClass.kt | 41 +++ 19 files changed, 1059 insertions(+), 2 deletions(-) create mode 100644 spring-auto-restdocs-dokka-json/.gitignore create mode 100644 spring-auto-restdocs-dokka-json/README.md create mode 100644 spring-auto-restdocs-dokka-json/pom.xml create mode 100644 spring-auto-restdocs-dokka-json/src/main/kotlin/capital/scalable/dokka/json/JsonFileGenerator.kt create mode 100644 spring-auto-restdocs-dokka-json/src/main/kotlin/capital/scalable/dokka/json/JsonFormatDescriptor.kt create mode 100644 spring-auto-restdocs-dokka-json/src/main/kotlin/capital/scalable/dokka/json/JsonFormatService.kt create mode 100644 spring-auto-restdocs-dokka-json/src/main/resources/dokka/format/auto-restdocs-json.properties create mode 100644 spring-auto-restdocs-dokka-json/src/test/kotlin/capital/scalable/dokka/json/FileComparisonFailure.kt create mode 100644 spring-auto-restdocs-dokka-json/src/test/kotlin/capital/scalable/dokka/json/JsonFileGeneratorTest.kt create mode 100644 spring-auto-restdocs-dokka-json/src/test/kotlin/capital/scalable/dokka/json/JsonFormatServiceTest.kt create mode 100644 spring-auto-restdocs-dokka-json/src/test/kotlin/capital/scalable/dokka/json/TestAPI.kt create mode 100644 spring-auto-restdocs-dokka-json/testdata/JavaClass.NestedJavaClass.json create mode 100644 spring-auto-restdocs-dokka-json/testdata/JavaClass.java create mode 100644 spring-auto-restdocs-dokka-json/testdata/JavaClass.json create mode 100644 spring-auto-restdocs-dokka-json/testdata/KotlinDataClass.NestedClass.json create mode 100644 spring-auto-restdocs-dokka-json/testdata/KotlinDataClass.json create mode 100644 spring-auto-restdocs-dokka-json/testdata/KotlinDataClass.kt diff --git a/pom.xml b/pom.xml index 2c783963..756c162e 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,7 @@ spring-auto-restdocs-core spring-auto-restdocs-example spring-auto-restdocs-docs + spring-auto-restdocs-dokka-json @@ -126,6 +127,8 @@ src/main/java src/test/java + src/main/kotlin + src/test/kotlin diff --git a/spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/jackson/FieldDocumentationObjectVisitor.java b/spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/jackson/FieldDocumentationObjectVisitor.java index 020df2b0..b972be04 100644 --- a/spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/jackson/FieldDocumentationObjectVisitor.java +++ b/spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/jackson/FieldDocumentationObjectVisitor.java @@ -7,9 +7,9 @@ * 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. @@ -51,6 +51,11 @@ public FieldDocumentationObjectVisitor(SerializerProvider provider, this.typeFactory = typeFactory; } + @Override + public void property(BeanProperty prop) throws JsonMappingException { + optionalProperty(prop); + } + @Override public void optionalProperty(BeanProperty prop) throws JsonMappingException { String jsonName = prop.getName(); diff --git a/spring-auto-restdocs-dokka-json/.gitignore b/spring-auto-restdocs-dokka-json/.gitignore new file mode 100644 index 00000000..21765a79 --- /dev/null +++ b/spring-auto-restdocs-dokka-json/.gitignore @@ -0,0 +1,29 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# IntelliJ +.idea/ +*.iml + +# Genertaed files +target/ diff --git a/spring-auto-restdocs-dokka-json/README.md b/spring-auto-restdocs-dokka-json/README.md new file mode 100644 index 00000000..f1feb162 --- /dev/null +++ b/spring-auto-restdocs-dokka-json/README.md @@ -0,0 +1,42 @@ +# KDoc and Javadoc to JSON converter for Spring Auto REST Docs + +This library is a [Dokka](https://github.com/Kotlin/dokka) extension. +Dokka is a documentation engine for Kotlin, performing the same function as Javadoc for Java. +Mixed-language Java/Kotlin projects are fully supported. +Dokka understands standard Javadoc comments in Java files and KDoc comments in Kotlin files. +The same holds true for this Dokka extension. + +## Usage with Maven + +This Dokka extension can be used with the standard `dokka-maven-plugin`. +To avoid any incompatibilities, the Dokka version of the `dokka-maven-plugin` and of this extension should be the same. +If this extension is included as a dependency of the plugin, the output format `auto-restdocs-json` can be used. + +Example usage: +``` + + org.jetbrains.dokka + dokka-maven-plugin + ${dokka.version} + + + compile + + dokka + + + + + + capital.scalable + spring-auto-restdocs-dokka-json + ${spring-auto-restdocs-dokka-json.version} + + + + auto-restdocs-json + ${jsonDirectory} + + +``` + diff --git a/spring-auto-restdocs-dokka-json/pom.xml b/spring-auto-restdocs-dokka-json/pom.xml new file mode 100644 index 00000000..8ef3404f --- /dev/null +++ b/spring-auto-restdocs-dokka-json/pom.xml @@ -0,0 +1,96 @@ + + 4.0.0 + + + capital.scalable + spring-auto-restdocs-parent + 1.0.12-SNAPSHOT + .. + + + spring-auto-restdocs-dokka-json + jar + + Spring Auto REST Docs Dokka JSON + Dokka extension that produces Spring Auto REST Docs' JSON format + https://github.com/scacap/spring-auto-restdocs + + + UTF-8 + 1.2.30 + 4.12 + + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-test-junit + ${kotlin.version} + test + + + junit + junit + ${junit.version} + test + + + org.jetbrains.dokka + dokka-fatjar + 0.9.16 + + + com.fasterxml.jackson.core + jackson-core + 2.9.4 + + + com.fasterxml.jackson.module + jackson-module-kotlin + 2.9.4.1 + + + + + src/main/kotlin + src/test/kotlin + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + + + + + jcenter + https://jcenter.bintray.com/ + + + + diff --git a/spring-auto-restdocs-dokka-json/src/main/kotlin/capital/scalable/dokka/json/JsonFileGenerator.kt b/spring-auto-restdocs-dokka-json/src/main/kotlin/capital/scalable/dokka/json/JsonFileGenerator.kt new file mode 100644 index 00000000..eebd8adf --- /dev/null +++ b/spring-auto-restdocs-dokka-json/src/main/kotlin/capital/scalable/dokka/json/JsonFileGenerator.kt @@ -0,0 +1,85 @@ +/*- + * #%L + * Spring Auto REST Docs Dokka JSON + * %% + * Copyright (C) 2015 - 2018 Scalable Capital GmbH + * %% + * 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. + * #L% + */ +package capital.scalable.dokka.json + +import com.google.inject.Inject +import org.jetbrains.dokka.* +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.OutputStreamWriter + +class JsonFileGenerator @Inject constructor(val locationService: FileLocationService) : Generator { + + @set:Inject(optional = true) + lateinit var formatService: FormatService + + override fun buildPages(nodes: Iterable) { + val specificLocationService = locationService.withExtension(formatService.extension) + + for ((_, items) in nodes.groupBy { specificLocationService.location(it) }) { + if (items.any { it.kind == NodeKind.Class }) { + val location = locationOverride(items.find { it.kind == NodeKind.Class }!!) + val file = location.file + file.parentFile?.mkdirsOrFail() + try { + FileOutputStream(file).use { + OutputStreamWriter(it, Charsets.UTF_8).use { + it.write(formatService.format(location, items)) + } + } + } catch (e: Throwable) { + println(e) + } + } + buildPages(items.flatMap { it.members }) + } + } + + private fun locationOverride(node: DocumentationNode): FileLocation { + val path = node.path + // Remove class name. It is appended again below. + .dropLast(1) + .filter { it.name.isNotEmpty() && it.kind != NodeKind.Module } + .joinToString("") { + if (it.kind == NodeKind.Class) { + // Parent class if the node is a nested class + "${it.name}." + } else { + // Turn package dots into folders + "${it.name.replace(".", "/")}/" + } + } + val className = node.name + return FileLocation(File(locationService.root.path, "$path$className.json")) + } + + override fun buildOutlines(nodes: Iterable) {} + + override fun buildSupportFiles() {} + + override fun buildPackageList(nodes: Iterable) {} + + private fun File.mkdirsOrFail() { + if (!mkdirs() && !exists()) { + throw IOException("Failed to create directory $this") + } + } +} diff --git a/spring-auto-restdocs-dokka-json/src/main/kotlin/capital/scalable/dokka/json/JsonFormatDescriptor.kt b/spring-auto-restdocs-dokka-json/src/main/kotlin/capital/scalable/dokka/json/JsonFormatDescriptor.kt new file mode 100644 index 00000000..7b39a487 --- /dev/null +++ b/spring-auto-restdocs-dokka-json/src/main/kotlin/capital/scalable/dokka/json/JsonFormatDescriptor.kt @@ -0,0 +1,40 @@ +/*- + * #%L + * Spring Auto REST Docs Dokka JSON + * %% + * Copyright (C) 2015 - 2018 Scalable Capital GmbH + * %% + * 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. + * #L% + */ +package capital.scalable.dokka.json + +import org.jetbrains.dokka.* +import org.jetbrains.dokka.Formats.FormatDescriptor +import org.jetbrains.dokka.Kotlin.KotlinDescriptorSignatureProvider +import org.jetbrains.dokka.Samples.DefaultSampleProcessingService +import org.jetbrains.dokka.Samples.SampleProcessingService +import kotlin.reflect.KClass + +class JsonFormatDescriptor : FormatDescriptor { + override val formatServiceClass = JsonFormatService::class + + override val packageDocumentationBuilderClass = KotlinPackageDocumentationBuilder::class + override val javaDocumentationBuilderClass = KotlinJavaDocumentationBuilder::class + + override val generatorServiceClass = JsonFileGenerator::class + override val outlineServiceClass: KClass? = null + override val sampleProcessingService: KClass = DefaultSampleProcessingService::class + override val packageListServiceClass: KClass? = DefaultPackageListService::class + override val descriptorSignatureProvider = KotlinDescriptorSignatureProvider::class +} diff --git a/spring-auto-restdocs-dokka-json/src/main/kotlin/capital/scalable/dokka/json/JsonFormatService.kt b/spring-auto-restdocs-dokka-json/src/main/kotlin/capital/scalable/dokka/json/JsonFormatService.kt new file mode 100644 index 00000000..27713bf3 --- /dev/null +++ b/spring-auto-restdocs-dokka-json/src/main/kotlin/capital/scalable/dokka/json/JsonFormatService.kt @@ -0,0 +1,131 @@ +/*- + * #%L + * Spring Auto REST Docs Dokka JSON + * %% + * Copyright (C) 2015 - 2018 Scalable Capital GmbH + * %% + * 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. + * #L% + */ +package capital.scalable.dokka.json + +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.google.inject.Inject +import org.jetbrains.dokka.* + +data class ClassDocumentation( + val comment: String, + val fields: Map, + val methods: Map) + +data class MethodDocumentation( + val comment: String, + val parameters: Map, + val tags: Map) + +data class FieldDocumentation( + val comment: String, + val tags: Map) + +open class JsonOutputBuilder( + private val to: StringBuilder, + private val logger: DokkaLogger) : FormattedOutputBuilder { + + private val mapper = jacksonObjectMapper() + .configure(SerializationFeature.INDENT_OUTPUT, true) + + override fun appendNodes(nodes: Iterable) { + val singleNode = nodes.singleOrNull() + when (singleNode?.kind) { + NodeKind.AllTypes -> Unit + NodeKind.GroupNode -> Unit + null -> Unit + else -> appendClassDocumentation(singleNode) + } + } + + private fun appendClassDocumentation(node: DocumentationNode) { + val fields = node.references(RefKind.Member) + .filter { it.to.kind == NodeKind.Property || it.to.kind == NodeKind.Field } + .map { propertyDocumentation(it.to) } + .toMap() + val methods = node.references(RefKind.Member) + .filter { it.to.kind == NodeKind.Function } + .map { functionDocumentation(it.to) } + .toMap() + val classDocumentation = ClassDocumentation( + comment = extractContent(node), + fields = fields, + methods = methods) + to.append(mapper.writeValueAsString(classDocumentation)) + } + + private fun propertyDocumentation(node: DocumentationNode): Pair { + return Pair(node.name, FieldDocumentation( + comment = extractContent(node), + tags = tags(node))) + } + + private fun functionDocumentation(node: DocumentationNode): Pair { + val parameterComments = node.content.sections + .filter { it.subjectName != null } + .map { + Pair(it.subjectName!!, extractContent(it)) + }.toMap() + return Pair(node.name, MethodDocumentation( + comment = extractContent(node), + parameters = parameterComments, + tags = tags(node))) + } + + private fun JsonOutputBuilder.tags(node: DocumentationNode): Map { + return node.content.sections + .map { Pair(tagName(it.tag), extractContent(it.children)) } + .toMap() + } + + private fun tagName(tag: String): String { + return when (tag) { + ContentTags.SeeAlso -> "see" + else -> tag.toLowerCase() + } + } + + private fun extractContent(node: DocumentationNode): String { + return extractContent(node.content.children) + } + + private fun extractContent(content: List): String { + return content.joinToString("") { extractContent(it) } + } + + private fun extractContent(content: ContentNode): String { + when (content) { + is ContentText -> return content.text + is ContentBlock -> return content.children.joinToString("") { extractContent(it) } + is ContentNodeLink -> return content.node?.let { extractContent(it) } ?: "" + is ContentEmpty -> return "" + else -> logger.warn("Unhandled content node: $content") + } + return "" + } +} + +open class JsonFormatService @Inject constructor(private val logger: DokkaLogger) : FormatService { + + override val extension: String = "json" + + override fun createOutputBuilder(to: StringBuilder, location: Location): FormattedOutputBuilder = + JsonOutputBuilder(to, logger) +} diff --git a/spring-auto-restdocs-dokka-json/src/main/resources/dokka/format/auto-restdocs-json.properties b/spring-auto-restdocs-dokka-json/src/main/resources/dokka/format/auto-restdocs-json.properties new file mode 100644 index 00000000..ce509e04 --- /dev/null +++ b/spring-auto-restdocs-dokka-json/src/main/resources/dokka/format/auto-restdocs-json.properties @@ -0,0 +1,2 @@ +class=capital.scalable.dokka.json.JsonFormatDescriptor +description=Turns KDoc and Javadoc into the Spring Auto REST Docs JSON format \ No newline at end of file diff --git a/spring-auto-restdocs-dokka-json/src/test/kotlin/capital/scalable/dokka/json/FileComparisonFailure.kt b/spring-auto-restdocs-dokka-json/src/test/kotlin/capital/scalable/dokka/json/FileComparisonFailure.kt new file mode 100644 index 00000000..42dacc37 --- /dev/null +++ b/spring-auto-restdocs-dokka-json/src/test/kotlin/capital/scalable/dokka/json/FileComparisonFailure.kt @@ -0,0 +1,54 @@ +/*- + * #%L + * Spring Auto REST Docs Dokka JSON + * %% + * Copyright (C) 2015 - 2018 Scalable Capital GmbH + * %% + * 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. + * #L% + */ +package capital.scalable.dokka.json + +import junit.framework.ComparisonFailure + +class FileComparisonFailure( + message: String, + expected: String?, + actual: String?, + expectedFilePath: String +) : ComparisonFailure(message, expected, actual) { + + private val myExpected: String + private val myActual: String + val filePath: String + + init { + when { + expected == null -> throw NullPointerException("'expected' must not be null") + actual == null -> throw NullPointerException("'actual' must not be null") + else -> { + this.myExpected = expected + this.myActual = actual + this.filePath = expectedFilePath + } + } + } + + override fun getExpected(): String { + return this.myExpected + } + + override fun getActual(): String { + return this.myActual + } +} diff --git a/spring-auto-restdocs-dokka-json/src/test/kotlin/capital/scalable/dokka/json/JsonFileGeneratorTest.kt b/spring-auto-restdocs-dokka-json/src/test/kotlin/capital/scalable/dokka/json/JsonFileGeneratorTest.kt new file mode 100644 index 00000000..3be8a953 --- /dev/null +++ b/spring-auto-restdocs-dokka-json/src/test/kotlin/capital/scalable/dokka/json/JsonFileGeneratorTest.kt @@ -0,0 +1,78 @@ +/*- + * #%L + * Spring Auto REST Docs Dokka JSON + * %% + * Copyright (C) 2015 - 2018 Scalable Capital GmbH + * %% + * 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. + * #L% + */ +package capital.scalable.dokka.json + +import com.intellij.openapi.util.io.FileUtil +import org.jetbrains.dokka.DocumentationModule +import org.jetbrains.dokka.DokkaConsoleLogger +import org.jetbrains.dokka.SingleFolderLocationService +import org.jetbrains.dokka.contentRootFromPath +import org.junit.Test +import java.io.File +import kotlin.test.assertTrue + +class JsonFileGeneratorTest { + + @Test + fun `buildPages should generate JSON files for Kotlin classes and nested classes`() { + val inputFile = "KotlinDataClass.kt" + val expectedFiles = listOf("testdata/KotlinDataClass", "testdata/KotlinDataClass.NestedClass") + verifyOutput(inputFile, verifier(expectedFiles)) + } + + @Test + fun `buildPages should generate JSON files for Java classes and nested classes`() { + val inputFile = "JavaClass.java" + val expectedFiles = listOf("JavaClass", "JavaClass.NestedJavaClass") + verifyJavaOutput(inputFile, verifier(expectedFiles)) + } + + private fun verifyOutput(inputFile: String, verifier: (DocumentationModule) -> Unit) { + verifyModel( + contentRootFromPath("testdata/$inputFile"), + verifier = verifier) + } + + private fun verifyJavaOutput(inputFile: String, verifier: (DocumentationModule) -> Unit) { + verifyJavaModel( + "testdata/$inputFile", + verifier = verifier) + } + + private fun verifier(expectedFiles: List): (DocumentationModule) -> Unit { + val fileGenerator = initFileGenerator() + val rootPath = File(fileGenerator.locationService.root.path) + return { + fileGenerator.buildPages(listOf(it)) + expectedFiles.forEach { + val file = rootPath.resolve("$it.json") + assertTrue("Expected file $it.json was not generated") { file.exists() } + } + } + } + + private fun initFileGenerator(): JsonFileGenerator { + val tempDir = FileUtil.createTempDirectory("dokka-json", "file-generator-test") + val fileLocationService = SingleFolderLocationService(tempDir, "json") + val fileGenerator = JsonFileGenerator(fileLocationService) + fileGenerator.formatService = JsonFormatService(DokkaConsoleLogger) + return fileGenerator + } +} diff --git a/spring-auto-restdocs-dokka-json/src/test/kotlin/capital/scalable/dokka/json/JsonFormatServiceTest.kt b/spring-auto-restdocs-dokka-json/src/test/kotlin/capital/scalable/dokka/json/JsonFormatServiceTest.kt new file mode 100644 index 00000000..44ae0ae0 --- /dev/null +++ b/spring-auto-restdocs-dokka-json/src/test/kotlin/capital/scalable/dokka/json/JsonFormatServiceTest.kt @@ -0,0 +1,81 @@ +/*- + * #%L + * Spring Auto REST Docs Dokka JSON + * %% + * Copyright (C) 2015 - 2018 Scalable Capital GmbH + * %% + * 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. + * #L% + */ +package capital.scalable.dokka.json + +import org.jetbrains.dokka.DocumentationModule +import org.jetbrains.dokka.DocumentationNode +import org.jetbrains.dokka.DokkaConsoleLogger +import org.jetbrains.dokka.NodeKind +import org.junit.Test + +private val tempLocation = InMemoryLocation("") + +class JsonFormatServiceTest { + + private val jsonFormatService = JsonFormatService(DokkaConsoleLogger) + + @Test + fun `Correct JSON file for a Kotlin data class should be created`() { + verifyJsonNode("KotlinDataClass") + } + + @Test + fun `Correct JSON file for a nested Kotlin data class should be created`() { + verifyNestedJsonNode("KotlinDataClass", ".NestedClass.json") + } + + @Test + fun `Correct JSON file for a Java class should be created`() { + verifyJavaJsonNode("JavaClass") + } + + @Test + fun `Correct JSON file for a nested Java class should be created`() { + verifyNestedJavaJsonNode("JavaClass", ".NestedJavaClass.json") + } + + private fun verifyJsonNode(fileName: String) { + verifyJsonNodes(fileName) { model -> model.members.single().members } + } + + private fun verifyNestedJsonNode(fileName: String, outputExtension: String) { + verifyJsonNodes(fileName, outputExtension) { model -> model.members.single().members.single().members(NodeKind.Class) } + } + + private fun verifyJsonNodes(fileName: String, outputExtension: String = ".json", nodeFilter: (DocumentationModule) -> List) { + verifyOutput("testdata/$fileName.kt", outputExtension) { model, output -> + jsonFormatService.createOutputBuilder(output, tempLocation).appendNodes(nodeFilter(model)) + } + } + + private fun verifyJavaJsonNode(fileName: String) { + verifyJavaJsonNodes(fileName) { model -> model.members.single().members } + } + + private fun verifyNestedJavaJsonNode(fileName: String, outputExtension: String) { + verifyJavaJsonNodes(fileName, outputExtension) { model -> model.members.single().members.single().members(NodeKind.Class) } + } + + private fun verifyJavaJsonNodes(fileName: String, outputExtension: String = ".json", nodeFilter: (DocumentationModule) -> List) { + verifyJavaOutput("testdata/$fileName.java", outputExtension) { model, output -> + jsonFormatService.createOutputBuilder(output, tempLocation).appendNodes(nodeFilter(model)) + } + } +} diff --git a/spring-auto-restdocs-dokka-json/src/test/kotlin/capital/scalable/dokka/json/TestAPI.kt b/spring-auto-restdocs-dokka-json/src/test/kotlin/capital/scalable/dokka/json/TestAPI.kt new file mode 100644 index 00000000..51dbfd7a --- /dev/null +++ b/spring-auto-restdocs-dokka-json/src/test/kotlin/capital/scalable/dokka/json/TestAPI.kt @@ -0,0 +1,261 @@ +/*- + * #%L + * Spring Auto REST Docs Dokka JSON + * %% + * Copyright (C) 2015 - 2018 Scalable Capital GmbH + * %% + * 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. + * #L% + */ +// Initially copied from https://github.com/Kotlin/dokka/blob/master/core/src/test/kotlin/TestAPI.kt +package capital.scalable.dokka.json + +import com.google.inject.Guice +import com.intellij.openapi.application.PathManager +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.io.FileUtil +import org.jetbrains.dokka.* +import org.jetbrains.dokka.Utilities.DokkaAnalysisModule +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot +import org.jetbrains.kotlin.config.ContentRoot +import org.jetbrains.kotlin.config.KotlinSourceRoot +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.junit.Assert +import java.io.File + +fun verifyModel(vararg roots: ContentRoot, + withJdk: Boolean = false, + withKotlinRuntime: Boolean = false, + format: String = "auto-restdocs-json", + includeNonPublic: Boolean = true, + perPackageOptions: List = emptyList(), + verifier: (DocumentationModule) -> Unit) { + val documentation = DocumentationModule("test") + + val options = DocumentationOptions( + "", + format, + includeNonPublic = includeNonPublic, + skipEmptyPackages = false, + includeRootPackage = true, + sourceLinks = listOf(), + perPackageOptions = perPackageOptions, + generateIndexPages = false, + noStdlibLink = true, + cacheRoot = "default", + languageVersion = null, + apiVersion = null + ) + + appendDocumentation(documentation, *roots, + withJdk = withJdk, + withKotlinRuntime = withKotlinRuntime, + options = options) + documentation.prepareForGeneration(options) + + verifier(documentation) +} + +fun appendDocumentation(documentation: DocumentationModule, + vararg roots: ContentRoot, + withJdk: Boolean = false, + withKotlinRuntime: Boolean = false, + options: DocumentationOptions, + defaultPlatforms: List = emptyList()) { + val messageCollector = object : MessageCollector { + override fun clear() { + + } + + override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageLocation?) { + when (severity) { + CompilerMessageSeverity.STRONG_WARNING, + CompilerMessageSeverity.WARNING, + CompilerMessageSeverity.LOGGING, + CompilerMessageSeverity.OUTPUT, + CompilerMessageSeverity.INFO, + CompilerMessageSeverity.ERROR -> { + println("$severity: $message at $location") + } + CompilerMessageSeverity.EXCEPTION -> { + Assert.fail("$severity: $message at $location") + } + } + } + + override fun hasErrors() = false + } + + val environment = AnalysisEnvironment(messageCollector) + environment.apply { + if (withJdk || withKotlinRuntime) { + val stringRoot = PathManager.getResourceRoot(String::class.java, "/java/lang/String.class") + addClasspath(File(stringRoot)) + } + if (withKotlinRuntime) { + val kotlinStrictfpRoot = PathManager.getResourceRoot(Strictfp::class.java, "/kotlin/jvm/Strictfp.class") + addClasspath(File(kotlinStrictfpRoot)) + } + addRoots(roots.toList()) + + loadLanguageVersionSettings(options.languageVersion, options.apiVersion) + } + val defaultPlatformsProvider = object : DefaultPlatformsProvider { + override fun getDefaultPlatforms(descriptor: DeclarationDescriptor) = defaultPlatforms + } + val injector = Guice.createInjector( + DokkaAnalysisModule(environment, options, defaultPlatformsProvider, documentation.nodeRefGraph, DokkaConsoleLogger)) + buildDocumentationModule(injector, documentation) + Disposer.dispose(environment) +} + +fun verifyJavaModel(source: String, + withKotlinRuntime: Boolean = false, + verifier: (DocumentationModule) -> Unit) { + val tempDir = FileUtil.createTempDirectory("dokka-json", "") + try { + val sourceFile = File(source) + FileUtil.copy(sourceFile, File(tempDir, sourceFile.name)) + verifyModel(JavaSourceRoot(tempDir, null), withJdk = true, withKotlinRuntime = withKotlinRuntime, verifier = verifier) + } + finally { + FileUtil.delete(tempDir) + } +} + +fun verifyOutput(roots: Array, + outputExtension: String, + withJdk: Boolean = false, + withKotlinRuntime: Boolean = false, + format: String = "auto-restdocs-json", + includeNonPublic: Boolean = true, + outputGenerator: (DocumentationModule, StringBuilder) -> Unit) { + verifyModel( + *roots, + withJdk = withJdk, + withKotlinRuntime = withKotlinRuntime, + format = format, + includeNonPublic = includeNonPublic + ) { + verifyModelOutput(it, outputExtension, roots.first().path, outputGenerator) + } +} + +fun verifyModelOutput(it: DocumentationModule, + outputExtension: String, + sourcePath: String, + outputGenerator: (DocumentationModule, StringBuilder) -> Unit) { + val output = StringBuilder() + outputGenerator(it, output) + val ext = outputExtension.removePrefix(".") + val expectedFile = File(sourcePath.replaceAfterLast(".", ext, "$sourcePath.$ext")) + assertEqualsIgnoringSeparators(expectedFile, output.toString()) +} + +fun verifyOutput(path: String, + outputExtension: String, + withJdk: Boolean = false, + withKotlinRuntime: Boolean = false, + format: String = "auto-restdocs-json", + includeNonPublic: Boolean = true, + outputGenerator: (DocumentationModule, StringBuilder) -> Unit) { + verifyOutput( + arrayOf(contentRootFromPath(path)), + outputExtension, + withJdk, + withKotlinRuntime, + format, + includeNonPublic, + outputGenerator + ) +} + +fun verifyJavaOutput(path: String, + outputExtension: String, + withKotlinRuntime: Boolean = false, + outputGenerator: (DocumentationModule, StringBuilder) -> Unit) { + verifyJavaModel(path, withKotlinRuntime) { model -> + verifyModelOutput(model, outputExtension, path, outputGenerator) + } +} + +fun assertEqualsIgnoringSeparators(expectedFile: File, output: String) { + if (!expectedFile.exists()) expectedFile.createNewFile() + val expectedText = expectedFile.readText().replace("\r\n", "\n") + val actualText = output.replace("\r\n", "\n") + + if(expectedText != actualText) + throw FileComparisonFailure("", expectedText, actualText, expectedFile.canonicalPath) +} + +fun StringBuilder.appendChildren(node: ContentBlock): StringBuilder { + for (child in node.children) { + val childText = child.toTestString() + append(childText) + } + return this +} + +fun StringBuilder.appendNode(node: ContentNode): StringBuilder { + when (node) { + is ContentText -> { + append(node.text) + } + is ContentEmphasis -> append("*").appendChildren(node).append("*") + is ContentBlockCode -> { + if (node.language.isNotBlank()) + appendln("[code lang=${node.language}]") + else + appendln("[code]") + appendChildren(node) + appendln() + appendln("[/code]") + } + is ContentNodeLink -> { + append("[") + appendChildren(node) + append(" -> ") + append(node.node.toString()) + append("]") + } + is ContentBlock -> { + appendChildren(node) + } + is ContentEmpty -> { /* nothing */ } + else -> throw IllegalStateException("Don't know how to format node $node") + } + return this +} + +fun ContentNode.toTestString(): String { + val node = this + return StringBuilder().apply { + appendNode(node) + }.toString() +} + +class InMemoryLocation(override val path: String): Location { + override fun relativePathTo(other: Location, anchor: String?): String = + if (anchor != null) other.path + "#" + anchor else other.path +} + +val ContentRoot.path: String + get() = when(this) { + is KotlinSourceRoot -> path + is JavaSourceRoot -> file.path + else -> throw UnsupportedOperationException() + } + diff --git a/spring-auto-restdocs-dokka-json/testdata/JavaClass.NestedJavaClass.json b/spring-auto-restdocs-dokka-json/testdata/JavaClass.NestedJavaClass.json new file mode 100644 index 00000000..8c87cd43 --- /dev/null +++ b/spring-auto-restdocs-dokka-json/testdata/JavaClass.NestedJavaClass.json @@ -0,0 +1,5 @@ +{ + "comment" : "A nested Java class", + "fields" : { }, + "methods" : { } +} \ No newline at end of file diff --git a/spring-auto-restdocs-dokka-json/testdata/JavaClass.java b/spring-auto-restdocs-dokka-json/testdata/JavaClass.java new file mode 100644 index 00000000..19bd6b80 --- /dev/null +++ b/spring-auto-restdocs-dokka-json/testdata/JavaClass.java @@ -0,0 +1,36 @@ +import java.math.BigDecimal; + +/** + * Javadoc on a Java class + */ +public class JavaClass { + + /** + * A Java field + * + * @see JavaClass + * @deprecated Use something else + * @title Custom tag + */ + public String someField; + + /** + * Method add + * + * @param a First param a + * @param b Second param b + * @see JavaClass + * @deprecated Use something else + * @title Custom tag + */ + public BigDecimal add(BigDecimal a, BigDecimal b) { + return a.add(b); + } + + /** + * A nested Java class + */ + public static class NestedJavaClass { + + } +} \ No newline at end of file diff --git a/spring-auto-restdocs-dokka-json/testdata/JavaClass.json b/spring-auto-restdocs-dokka-json/testdata/JavaClass.json new file mode 100644 index 00000000..d8943f71 --- /dev/null +++ b/spring-auto-restdocs-dokka-json/testdata/JavaClass.json @@ -0,0 +1,26 @@ +{ + "comment" : "Javadoc on a Java class", + "fields" : { + "someField" : { + "comment" : "A Java field", + "tags" : { + "see" : "JavaClass", + "title" : "Custom tag" + } + } + }, + "methods" : { + "add" : { + "comment" : "Method add", + "parameters" : { + "a" : "First param a", + "b" : "Second param b" + }, + "tags" : { + "parameters" : "Second param b", + "see" : "JavaClass", + "title" : "Custom tag" + } + } + } +} \ No newline at end of file diff --git a/spring-auto-restdocs-dokka-json/testdata/KotlinDataClass.NestedClass.json b/spring-auto-restdocs-dokka-json/testdata/KotlinDataClass.NestedClass.json new file mode 100644 index 00000000..6956b720 --- /dev/null +++ b/spring-auto-restdocs-dokka-json/testdata/KotlinDataClass.NestedClass.json @@ -0,0 +1,10 @@ +{ + "comment" : "A nested class", + "fields" : { + "someField" : { + "comment" : "Field on a nested class", + "tags" : { } + } + }, + "methods" : { } +} \ No newline at end of file diff --git a/spring-auto-restdocs-dokka-json/testdata/KotlinDataClass.json b/spring-auto-restdocs-dokka-json/testdata/KotlinDataClass.json new file mode 100644 index 00000000..118f3f15 --- /dev/null +++ b/spring-auto-restdocs-dokka-json/testdata/KotlinDataClass.json @@ -0,0 +1,32 @@ +{ + "comment" : "Class documentation", + "fields" : { + "text" : { + "comment" : "Documentation for text property", + "tags" : { + "see" : "KotlinDataClass", + "deprecated" : "Use something else", + "title" : "Custom tag" + } + }, + "number" : { + "comment" : "Documentation for number property", + "tags" : { } + } + }, + "methods" : { + "add" : { + "comment" : "Function add", + "parameters" : { + "a" : "First param a", + "b" : "Second param b" + }, + "tags" : { + "parameters" : "Second param b", + "see" : "KotlinDataClass", + "deprecated" : "Use something else", + "title" : "Custom tag" + } + } + } +} \ No newline at end of file diff --git a/spring-auto-restdocs-dokka-json/testdata/KotlinDataClass.kt b/spring-auto-restdocs-dokka-json/testdata/KotlinDataClass.kt new file mode 100644 index 00000000..6b7efe57 --- /dev/null +++ b/spring-auto-restdocs-dokka-json/testdata/KotlinDataClass.kt @@ -0,0 +1,41 @@ +package testdata + +import java.math.BigDecimal + +/** + * Class documentation + */ +data class KotlinDataClass( + /** + * Documentation for text property + * + * @see KotlinDataClass + * @deprecated Use something else + * @title Custom tag + */ + val text: String, + /** + * Documentation for number property + */ + val number: BigDecimal) { + + /** + * Function add + * + * @param a First param a + * @param b Second param b + * @see KotlinDataClass + * @deprecated Use something else + * @title Custom tag + */ + fun add(a: BigDecimal, b: BigDecimal): BigDecimal = a + b + + /** + * A nested class + */ + data class NestedClass( + /** + * Field on a nested class + */ + val someField: String) +} \ No newline at end of file