Unverified Commit be16c2f2 authored by Russell Cohen's avatar Russell Cohen Committed by GitHub
Browse files

Generate documentation for structures (#47)

* Generate documentation for structures

The first of many commits to generate docs. This is a first pass for structures, we still need to document enums & unions. Some serious design also needs to occur to figure out the best practice for turning the Smithy documentation into nice Rust documentation.

* Bump Java version to 9

* Remove exclusion of `target`

* Don't build docs for deps

* Update to use getMemberTrait
parent 8884657c
Loading
Loading
Loading
Loading
+5 −4
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@ name: CI

env:
  rust_version: 1.48.0
  java_version: 9

jobs:
  style:
@@ -14,7 +15,7 @@ jobs:
    - name: Set up JDK
      uses: actions/setup-java@v1
      with:
        java-version: 8
        java-version: ${{ env.java_version }}
    - uses: actions/cache@v2
      with:
        path: |
@@ -51,7 +52,7 @@ jobs:
    - name: Set up JDK
      uses: actions/setup-java@v1
      with:
        java-version: 8
        java-version: ${{ env.java_version }}
    - name: test
      run: ./gradlew :codegen:test
  integration-tests:
@@ -82,7 +83,7 @@ jobs:
    - name: Set up JDK
      uses: actions/setup-java@v1
      with:
        java-version: 8
        java-version: ${{ env.java_version }}
    - name: integration-tests
      run: ./gradlew :codegen-test:test
    - uses: actions/upload-artifact@v2
@@ -94,7 +95,7 @@ jobs:
        path: |
          codegen-test/build/smithyprojections/codegen-test/*/rust-codegen/
          codegen-test/build/smithyprojections/codegen-test/Cargo.toml
          !**/target
          codegen-test/build/smithyprojections/codegen-test/target/doc
  runtime-tests:
    name: Rust runtime tests
    runs-on: ubuntu-latest
+9 −1
Original line number Diff line number Diff line
@@ -110,6 +110,14 @@ tasks.register<Exec>("cargoTest") {
    dependsOn("build")
}

tasks.register<Exec>("cargoDocs") {
    workingDir("build/smithyprojections/codegen-test/")
    // disallow warnings
    environment("RUSTFLAGS", "-D warnings")
    commandLine("cargo", "doc", "--no-deps")
    dependsOn("build")
}

tasks.register<Exec>("cargoClippy") {
    workingDir("build/smithyprojections/codegen-test/")
    // disallow warnings
@@ -118,7 +126,7 @@ tasks.register<Exec>("cargoClippy") {
    dependsOn("build")
}

tasks["test"].finalizedBy("cargoCheck", "cargoClippy", "cargoTest")
tasks["test"].finalizedBy("cargoCheck", "cargoClippy", "cargoTest", "cargoDocs")

tasks["clean"].doFirst {
    delete("smithy-build.json")
+12 −0
Original line number Diff line number Diff line
@@ -5,4 +5,16 @@ data class RustModule(val name: String, val rustMetadata: RustMetadata) {
        rustMetadata.render(writer)
        writer.write("mod $name;")
    }

    companion object {
        fun default(name: String, public: Boolean): RustModule {
            // TODO: figure out how to enable this, but only for real services (protocol tests don't have documentation)
            /*val attributes = if (public) {
                listOf(Custom("deny(missing_docs)"))
            } else {
                listOf()
            }*/
            return RustModule(name, RustMetadata(public = public))
        }
    }
}
+72 −9
Original line number Diff line number Diff line
@@ -10,13 +10,16 @@ import software.amazon.smithy.codegen.core.CodegenException
import software.amazon.smithy.codegen.core.Symbol
import software.amazon.smithy.codegen.core.writer.CodegenWriter
import software.amazon.smithy.codegen.core.writer.CodegenWriterFactory
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.CollectionShape
import software.amazon.smithy.model.shapes.Shape
import software.amazon.smithy.model.shapes.ShapeId
import software.amazon.smithy.model.traits.DocumentationTrait
import software.amazon.smithy.model.traits.EnumTrait
import software.amazon.smithy.rust.codegen.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.smithy.isOptional
import software.amazon.smithy.rust.codegen.smithy.rustType
import software.amazon.smithy.rust.codegen.util.orNull
import software.amazon.smithy.utils.CodeWriter
import java.util.function.BiFunction

@@ -74,11 +77,55 @@ fun <T : CodeWriter> T.rustBlock(header: String, vararg args: Any, block: T.() -
    return this
}

class RustWriter private constructor(private val filename: String, val namespace: String, private val commentCharacter: String = "//", private val printWarning: Boolean = true) :
/**
 * Generate a RustDoc comment for [shape]
 */
fun <T : CodeWriter> T.documentShape(shape: Shape, model: Model): T {
    // TODO: support additional Smithy documentation traits like @example
    val docTrait = shape.getMemberTrait(model, DocumentationTrait::class.java).orNull()

    docTrait?.value?.also {
        this.docs(it)
    }

    return this
}

/**
 * Write RustDoc-style docs into the writer
 *
 * Several modifications are made to provide consistent RustDoc formatting:
 *    - All lines will be prefixed by `///`
 *    - Tabs are replaced with spaces
 *    - Empty newlines are removed
 */
fun <T : CodeWriter> T.docs(text: String, vararg args: Any) {
    pushState("docs")
    setNewlinePrefix("/// ")
    val cleaned = text.lines()
        // We need to filter out blank lines—an empty line causes the markdown parser to interpret the subsequent
        // docs as a code block because they are indented.
        .filter { !it.isBlank() }
        .joinToString("\n") {
            // Rustdoc warns on tabs in documentation
            it.trimStart().replace("\t", "  ")
        }
    write(cleaned, *args)
    popState()
}

class RustWriter private constructor(
    private val filename: String,
    val namespace: String,
    private val commentCharacter: String = "//",
    private val printWarning: Boolean = true
) :
    CodegenWriter<RustWriter, UseDeclarations>(null, UseDeclarations(namespace)) {
    companion object {
        fun forModule(module: String): RustWriter {
            return RustWriter("$module.rs", "crate::$module")
        fun forModule(module: String?): RustWriter = if (module == null) {
            RustWriter("lib.rs", "crate")
        } else {
            RustWriter("$module.rs", "crate::$module")
        }

        val Factory: CodegenWriterFactory<RustWriter> =
@@ -89,17 +136,16 @@ class RustWriter private constructor(private val filename: String, val namespace
                }
            }
    }
    init {
        if (filename.endsWith(".rs")) {
            require(namespace.startsWith("crate")) { "We can only write into files in the crate (got $namespace)" }
        }
    }

    private val formatter = RustSymbolFormatter()
    private var n = 0

    init {
        if (filename.endsWith(".rs")) {
            require(namespace.startsWith("crate")) { "We can only write into files in the crate (got $namespace)" }
        }
        putFormatter('T', formatter)
        putFormatter('D', RustDocLinker())
    }

    fun module(): String? = if (filename.endsWith(".rs")) {
@@ -125,7 +171,11 @@ class RustWriter private constructor(private val filename: String, val namespace
     *
     * The returned writer will inject any local imports into the module as needed.
     */
    fun withModule(moduleName: String, rustMetadata: RustMetadata = RustMetadata(public = true), moduleWriter: RustWriter.() -> Unit) {
    fun withModule(
        moduleName: String,
        rustMetadata: RustMetadata = RustMetadata(public = true),
        moduleWriter: RustWriter.() -> Unit
    ): RustWriter {
        // In Rust, modules must specify their own imports—they don't have access to the parent scope.
        // To easily handle this, create a new inner writer to collect imports, then dump it
        // into an inline module.
@@ -136,6 +186,7 @@ class RustWriter private constructor(private val filename: String, val namespace
            write(innerWriter.toString())
        }
        innerWriter.dependencies.forEach { addDependency(it) }
        return this
    }

    // TODO: refactor both of these methods & add a parent method to for_each across any field type
@@ -186,6 +237,18 @@ class RustWriter private constructor(private val filename: String, val namespace
        }
    }

    /**
     * Generate RustDoc links, eg. [`Abc`](crate::module::Abc)
     */
    inner class RustDocLinker : BiFunction<Any, String, String> {
        override fun apply(t: Any, u: String): String {
            return when (t) {
                is Symbol -> "[`${t.name}`](${t.fullName})"
                else -> throw CodegenException("Invalid type provided to RustDocLinker ($t) expected Symbol")
            }
        }
    }

    inner class RustSymbolFormatter : BiFunction<Any, String, String> {
        override fun apply(t: Any, u: String): String {
            return when (t) {
+2 −3
Original line number Diff line number Diff line
@@ -20,7 +20,6 @@ import software.amazon.smithy.model.traits.EnumTrait
import software.amazon.smithy.rust.codegen.lang.CargoDependency
import software.amazon.smithy.rust.codegen.lang.InlineDependency
import software.amazon.smithy.rust.codegen.lang.RustDependency
import software.amazon.smithy.rust.codegen.lang.RustMetadata
import software.amazon.smithy.rust.codegen.lang.RustModule
import software.amazon.smithy.rust.codegen.lang.RustWriter
import software.amazon.smithy.rust.codegen.smithy.generators.CargoTomlGenerator
@@ -103,8 +102,8 @@ class CodegenVisitor(context: PluginContext) : ShapeVisitor.Default<Unit>() {
        }
        writers.useFileWriter("src/lib.rs", "crate::lib") { writer ->
            val includedModules = writers.includedModules().toSet().filter { it != "lib" }
            val modules = includedModules.map {
                RustModule(it, RustMetadata(public = PublicModules.contains(it)))
            val modules = includedModules.map { moduleName ->
                RustModule.default(moduleName, PublicModules.contains(moduleName))
            }
            LibRsGenerator(modules).render(writer)
        }
Loading