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
This commit is contained in:
Russell Cohen 2020-11-24 11:26:11 -05:00 committed by GitHub
parent 8884657c3a
commit be16c2f225
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 217 additions and 57 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,8 +13,11 @@ import software.amazon.smithy.model.traits.ErrorTrait
import software.amazon.smithy.rust.codegen.lang.RustType
import software.amazon.smithy.rust.codegen.lang.RustWriter
import software.amazon.smithy.rust.codegen.lang.conditionalBlock
import software.amazon.smithy.rust.codegen.lang.docs
import software.amazon.smithy.rust.codegen.lang.documentShape
import software.amazon.smithy.rust.codegen.lang.render
import software.amazon.smithy.rust.codegen.lang.rustBlock
import software.amazon.smithy.rust.codegen.lang.stripOuter
import software.amazon.smithy.rust.codegen.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.smithy.canUseDefault
import software.amazon.smithy.rust.codegen.smithy.expectRustMetadata
@ -24,8 +27,6 @@ import software.amazon.smithy.rust.codegen.smithy.rustType
import software.amazon.smithy.rust.codegen.util.dq
import software.amazon.smithy.utils.CaseUtils
// TODO(maybe): extract struct generation from Smithy shapes to support generating body objects
// TODO: generate documentation
class StructureGenerator(
val model: Model,
private val symbolProvider: SymbolProvider,
@ -47,6 +48,8 @@ class StructureGenerator(
}
if (renderBuilder) {
val symbol = symbolProvider.toSymbol(shape)
// TODO: figure out exactly what docs we want on a the builder module
writer.docs("See \$D", symbol)
writer.withModule(symbol.name.toSnakeCase()) {
renderBuilder(this)
}
@ -83,13 +86,14 @@ class StructureGenerator(
private fun renderStructure() {
val symbol = symbolProvider.toSymbol(shape)
// TODO(maybe): Pull derive info from the symbol so that the symbol provider can alter things as necessary; 4h
val containerMeta = symbol.expectRustMetadata()
writer.documentShape(shape, model)
containerMeta.render(writer)
writer.rustBlock("struct ${symbol.name} ${lifetimeDeclaration()}") {
members.forEach { member ->
val memberName = symbolProvider.toMemberName(member)
writer.documentShape(member, model)
symbolProvider.toSymbol(member).expectRustMetadata().render(this)
write("$memberName: \$T,", symbolProvider.toSymbol(member))
}
@ -97,6 +101,7 @@ class StructureGenerator(
if (renderBuilder) {
writer.rustBlock("impl ${symbol.name}") {
docs("Creates a new builder-style object to manufacture \$D", symbol)
rustBlock("pub fn builder() -> \$T", builderSymbol) {
write("\$T::default()", builderSymbol)
}
@ -105,11 +110,10 @@ class StructureGenerator(
}
private fun renderBuilder(writer: RustWriter) {
// Eventually, I want to do a fancier module layout:
// model/some_model.rs [contains builder and impl for a single model] struct SomeModel, struct Builder
// model/mod.rs [contains pub use for each model to bring it into top level scope]
// users will do models::SomeModel, models::SomeModel::builder()
val builderName = "Builder"
val symbol = symbolProvider.toSymbol(shape)
writer.docs("A builder for \$D", symbol)
writer.write("#[non_exhaustive]")
writer.write("#[derive(Debug, Clone, Default)]")
writer.rustBlock("pub struct $builderName") {
@ -134,17 +138,13 @@ class StructureGenerator(
// All fields in the builder are optional
val memberSymbol = symbolProvider.toSymbol(member)
val outerType = memberSymbol.rustType()
val coreType = outerType.let {
when (it) {
is RustType.Option -> it.value
else -> it
}
}
val coreType = outerType.stripOuter<RustType.Option>()
val signature = when (coreType) {
is RustType.String -> "<Str: Into<String>>(mut self, inp: Str) -> Self"
is RustType.Box -> "<T>(mut self, inp: T) -> Self where T: Into<${coreType.render()}>"
else -> "(mut self, inp: ${coreType.render()}) -> Self"
}
writer.documentShape(member, model)
writer.rustBlock("pub fn $memberName$signature") {
write("self.$memberName = Some(${builderConverter(coreType)});")
write("self")
@ -157,6 +157,7 @@ class StructureGenerator(
false -> "\$T"
}
writer.docs("Consumes the builder and constructs a \$D", symbol)
rustBlock("pub fn build(self) -> $returnType", structureSymbol) {
conditionalBlock("Ok(", ")", conditional = fallibleBuilder) {
rustBlock("\$T", structureSymbol) {

View File

@ -33,7 +33,7 @@ import software.amazon.smithy.rust.codegen.smithy.Shapes
import software.amazon.smithy.rust.codegen.smithy.isOptional
import software.amazon.smithy.rust.codegen.smithy.referenceClosure
import software.amazon.smithy.rust.codegen.smithy.rustType
import software.amazon.smithy.rust.testutil.asSmithy
import software.amazon.smithy.rust.testutil.asSmithyModel
import software.amazon.smithy.rust.testutil.testSymbolProvider
class SymbolBuilderTest {
@ -91,7 +91,7 @@ class SymbolBuilderTest {
}
])
string StandardUnit
""".asSmithy()
""".asSmithyModel()
val shape = model.expectShape(ShapeId.from("test#StandardUnit"))
val provider: SymbolProvider = testSymbolProvider(model)
val sym = provider.toSymbol(shape)
@ -257,7 +257,7 @@ class SymbolBuilderTest {
// Sent in the body
additional: String,
}
""".asSmithy()
""".asSmithyModel()
val symbol = testSymbolProvider(model).toSymbol(model.expectShape(ShapeId.from("smithy.example#PutObject")))
symbol.definitionFile shouldBe("src/${Operations.filename}")
symbol.name shouldBe "PutObject"

View File

@ -16,7 +16,7 @@ import software.amazon.smithy.model.traits.EnumTrait
import software.amazon.smithy.rust.codegen.lang.RustWriter
import software.amazon.smithy.rust.codegen.smithy.generators.EnumGenerator
import software.amazon.smithy.rust.codegen.util.lookup
import software.amazon.smithy.rust.testutil.asSmithy
import software.amazon.smithy.rust.testutil.asSmithyModel
import software.amazon.smithy.rust.testutil.compileAndRun
import software.amazon.smithy.rust.testutil.compileAndTest
import software.amazon.smithy.rust.testutil.shouldCompile
@ -81,7 +81,7 @@ class EnumGeneratorTest {
name: "Bar"
}])
string FooEnum
""".asSmithy()
""".asSmithyModel()
val shape: StringShape = model.lookup("test#FooEnum")
val trait = shape.expectTrait(EnumTrait::class.java)
val writer = RustWriter.forModule("model")
@ -109,7 +109,7 @@ class EnumGeneratorTest {
value: "Bar",
}])
string FooEnum
""".asSmithy()
""".asSmithyModel()
val shape: StringShape = model.lookup("test#FooEnum")
val trait = shape.expectTrait(EnumTrait::class.java)
val writer = RustWriter.forModule("model")
@ -147,7 +147,7 @@ class EnumGeneratorTest {
},
])
string FooEnum
""".asSmithy()
""".asSmithyModel()
val shape = model.expectShape(ShapeId.from("test#FooEnum"), StringShape::class.java)
val trait = shape.expectTrait(EnumTrait::class.java)
val provider: SymbolProvider = testSymbolProvider(model)

View File

@ -30,7 +30,7 @@ import software.amazon.smithy.rust.codegen.smithy.generators.uriFormatString
import software.amazon.smithy.rust.codegen.smithy.transformers.OperationNormalizer
import software.amazon.smithy.rust.codegen.util.dq
import software.amazon.smithy.rust.testutil.TestRuntimeConfig
import software.amazon.smithy.rust.testutil.asSmithy
import software.amazon.smithy.rust.testutil.asSmithyModel
import software.amazon.smithy.rust.testutil.compileAndTest
import software.amazon.smithy.rust.testutil.testSymbolProvider
@ -89,7 +89,7 @@ class HttpTraitBindingGeneratorTest {
// Sent in the body
additional: String,
}
""".asSmithy()
""".asSmithyModel()
private val model = OperationNormalizer().transformModel(baseModel)
private val operationShape = model.expectShape(ShapeId.from("smithy.example#PutObject"), OperationShape::class.java)

View File

@ -12,11 +12,15 @@ import software.amazon.smithy.model.shapes.MemberShape
import software.amazon.smithy.model.shapes.Shape
import software.amazon.smithy.model.shapes.ShapeId
import software.amazon.smithy.model.shapes.StructureShape
import software.amazon.smithy.rust.codegen.lang.Custom
import software.amazon.smithy.rust.codegen.lang.RustMetadata
import software.amazon.smithy.rust.codegen.lang.RustWriter
import software.amazon.smithy.rust.codegen.lang.docs
import software.amazon.smithy.rust.codegen.lang.rustBlock
import software.amazon.smithy.rust.codegen.smithy.canUseDefault
import software.amazon.smithy.rust.codegen.smithy.generators.StructureGenerator
import software.amazon.smithy.rust.testutil.asSmithy
import software.amazon.smithy.rust.codegen.util.lookup
import software.amazon.smithy.rust.testutil.asSmithyModel
import software.amazon.smithy.rust.testutil.compileAndTest
import software.amazon.smithy.rust.testutil.testSymbolProvider
@ -42,7 +46,7 @@ class StructureGeneratorTest {
structure MyError {
message: String
}
""".asSmithy()
""".asSmithyModel()
private val struct = model.expectShape(ShapeId.from("com.test#MyStruct"), StructureShape::class.java)
private val inner = model.expectShape(ShapeId.from("com.test#Inner"), StructureShape::class.java)
private val error = model.expectShape(ShapeId.from("com.test#MyError"), StructureShape::class.java)
@ -141,4 +145,37 @@ class StructureGeneratorTest {
generator.render()
writer.compileAndTest()
}
@Test
fun `attach docs to everything`() {
val model = """
namespace com.test
@documentation("inner doc")
structure Inner { }
@documentation("shape doc")
structure MyStruct {
@documentation("member doc")
member: String,
@documentation("specific docs")
nested: Inner,
nested2: Inner
}""".asSmithyModel()
val provider: SymbolProvider = testSymbolProvider(model)
val writer = RustWriter.forModule(null)
writer.docs("module docs")
writer
.withModule(
"model",
// By attaching this lint, any missing documentation becomes a compiler erorr
RustMetadata(additionalAttributes = listOf(Custom("deny(missing_docs)")), public = true)
) {
StructureGenerator(model, provider, this, model.lookup("com.test#Inner")).render()
StructureGenerator(model, provider, this, model.lookup("com.test#MyStruct")).render()
}
writer.compileAndTest()
}
}

View File

@ -13,7 +13,7 @@ import software.amazon.smithy.rust.codegen.util.CommandFailed
import software.amazon.smithy.rust.codegen.util.dq
import software.amazon.smithy.rust.codegen.util.lookup
import software.amazon.smithy.rust.testutil.TestRuntimeConfig
import software.amazon.smithy.rust.testutil.asSmithy
import software.amazon.smithy.rust.testutil.asSmithyModel
import software.amazon.smithy.rust.testutil.compileAndTest
import software.amazon.smithy.rust.testutil.testSymbolProvider
@ -69,7 +69,7 @@ class HttpProtocolTestGeneratorTest {
name: String
}
""".asSmithy()
""".asSmithyModel()
private val model = OperationNormalizer().transformModel(baseModel)
private val symbolProvider = testSymbolProvider(model)
private val runtimeConfig = TestRuntimeConfig

View File

@ -15,7 +15,7 @@ import software.amazon.smithy.rust.codegen.smithy.transformers.RecursiveShapeBox
import software.amazon.smithy.rust.codegen.util.dq
import software.amazon.smithy.rust.codegen.util.lookup
import software.amazon.smithy.rust.testutil.TestRuntimeConfig
import software.amazon.smithy.rust.testutil.asSmithy
import software.amazon.smithy.rust.testutil.asSmithyModel
import software.amazon.smithy.rust.testutil.compileAndTest
import software.amazon.smithy.rust.testutil.testSymbolProvider
@ -59,7 +59,7 @@ class InstantiatorTest {
member: WithBox,
value: Integer
}
""".asSmithy().let { RecursiveShapeBoxer.transform(it) }
""".asSmithyModel().let { RecursiveShapeBoxer.transform(it) }
private val symbolProvider = testSymbolProvider(model)
private val runtimeConfig = TestRuntimeConfig

View File

@ -13,7 +13,7 @@ import software.amazon.smithy.model.shapes.StructureShape
import software.amazon.smithy.rust.codegen.smithy.traits.InputBodyTrait
import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticInputTrait
import software.amazon.smithy.rust.codegen.util.lookup
import software.amazon.smithy.rust.testutil.asSmithy
import software.amazon.smithy.rust.testutil.asSmithyModel
import software.amazon.smithy.rust.testutil.testSymbolProvider
internal class OperationNormalizerTest {
@ -23,7 +23,7 @@ internal class OperationNormalizerTest {
val model = """
namespace smithy.test
operation Empty {}
""".asSmithy()
""".asSmithyModel()
val operationId = ShapeId.from("smithy.test#Empty")
model.expectShape(operationId, OperationShape::class.java).input.isPresent shouldBe false
val sut = OperationNormalizer()
@ -48,7 +48,7 @@ internal class OperationNormalizerTest {
operation MyOp {
input: RenameMe
}
""".asSmithy()
""".asSmithyModel()
val operationId = ShapeId.from("smithy.test#MyOp")
model.expectShape(operationId, OperationShape::class.java).input.isPresent shouldBe true
val sut = OperationNormalizer()
@ -72,7 +72,7 @@ internal class OperationNormalizerTest {
}
operation MyOp {
input: RenameMe
}""".asSmithy()
}""".asSmithyModel()
val sut = OperationNormalizer()
val modified = sut.transformModel(model) { input ->

View File

@ -5,7 +5,7 @@ import org.junit.jupiter.api.Test
import software.amazon.smithy.model.shapes.MemberShape
import software.amazon.smithy.rust.codegen.smithy.RustBoxTrait
import software.amazon.smithy.rust.codegen.util.lookup
import software.amazon.smithy.rust.testutil.asSmithy
import software.amazon.smithy.rust.testutil.asSmithyModel
import kotlin.streams.toList
internal class RecursiveShapeBoxerTest {
@ -23,7 +23,7 @@ internal class RecursiveShapeBoxerTest {
structure Bar {
hello: Hello
}
""".asSmithy()
""".asSmithyModel()
RecursiveShapeBoxer.transform(model) shouldBe model
}
@ -35,7 +35,7 @@ internal class RecursiveShapeBoxerTest {
RecursiveStruct: Recursive,
anotherField: Boolean
}
""".asSmithy()
""".asSmithyModel()
val transformed = RecursiveShapeBoxer.transform(model)
val member: MemberShape = transformed.lookup("com.example#Recursive\$RecursiveStruct")
member.expectTrait(RustBoxTrait::class.java)
@ -62,7 +62,7 @@ internal class RecursiveShapeBoxerTest {
otherMember: Atom,
third: SecondTree
}
""".asSmithy()
""".asSmithyModel()
val transformed = RecursiveShapeBoxer.transform(model)
val boxed = transformed.shapes().filter { it.hasTrait(RustBoxTrait::class.java) }.toList()
boxed.map { it.id.toString().removePrefix("com.example#") }.toSet() shouldBe setOf(

View File

@ -10,7 +10,7 @@ import software.amazon.smithy.rust.codegen.smithy.generators.StructureGenerator
import software.amazon.smithy.rust.codegen.smithy.generators.UnionGenerator
import software.amazon.smithy.rust.codegen.util.CommandFailed
import software.amazon.smithy.rust.codegen.util.lookup
import software.amazon.smithy.rust.testutil.asSmithy
import software.amazon.smithy.rust.testutil.asSmithyModel
import software.amazon.smithy.rust.testutil.compileAndTest
import software.amazon.smithy.rust.testutil.testSymbolProvider
@ -36,7 +36,7 @@ class RecursiveShapesIntegrationTest {
otherMember: Atom,
third: SecondTree
}
""".asSmithy()
""".asSmithyModel()
val check = { input: Model ->
val structures = listOf("Expr", "SecondTree").map { input.lookup<StructureShape>("com.example#$it") }
val writer = RustWriter.forModule("model")

View File

@ -13,12 +13,17 @@ import software.amazon.smithy.codegen.core.SymbolProvider
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.SetShape
import software.amazon.smithy.model.shapes.StringShape
import software.amazon.smithy.model.shapes.StructureShape
import software.amazon.smithy.rust.codegen.lang.CargoDependency
import software.amazon.smithy.rust.codegen.lang.RustType
import software.amazon.smithy.rust.codegen.lang.RustWriter
import software.amazon.smithy.rust.codegen.lang.docs
import software.amazon.smithy.rust.codegen.lang.rustBlock
import software.amazon.smithy.rust.codegen.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.util.lookup
import software.amazon.smithy.rust.testutil.asSmithyModel
import software.amazon.smithy.rust.testutil.compileAndRun
import software.amazon.smithy.rust.testutil.compileAndTest
import software.amazon.smithy.rust.testutil.shouldCompile
import software.amazon.smithy.rust.testutil.shouldMatchResource
import software.amazon.smithy.rust.testutil.shouldParseAsRust
@ -78,4 +83,33 @@ class RustWriterTest {
"""
)
}
@Test
fun `generate docs`() {
val sut = RustWriter.forModule("lib")
sut.docs(
"""Top level module documentation
|More docs
|/* handle weird characters */
|`a backtick`
|[a link](asdf)
""".trimMargin()
)
sut.rustBlock("fn main()") { }
sut.compileAndTest()
sut.toString() shouldContain "Top level module"
}
@Test
fun `generate doc links`() {
val model = """
namespace test
structure Foo {}
""".asSmithyModel()
val shape = model.lookup<StructureShape>("test#Foo")
val symbol = testSymbolProvider(model).toSymbol(shape)
val sut = RustWriter.forModule("lib")
sut.docs("A link! \$D", symbol)
sut.toString() shouldContain "/// A link! [`Foo`](crate::model::Foo)"
}
}

View File

@ -69,8 +69,13 @@ fun RustWriter.compileAndTest(
// TODO: if there are no dependencies, we can be a bit quicker
val deps = this.dependencies.map { RustDependency.fromSymbolDependency(it) }.filterIsInstance<CargoDependency>()
try {
val module = if (this.namespace.contains("::")) {
this.namespace.split("::")[1]
} else {
"lib"
}
val output = this.toString()
.compileAndTest(deps.toSet(), module = this.namespace.split("::")[1], main = main, strict = clippy)
.compileAndTest(deps.toSet(), module = module, main = main, strict = clippy)
if (expectFailure) {
println(this.toString())
}

View File

@ -19,7 +19,7 @@ val TestSymbolVisitorConfig = SymbolVisitorConfig(runtimeConfig = TestRuntimeCon
fun testSymbolProvider(model: Model): SymbolProvider = RustCodegenPlugin.BaseSymbolProvider(model, TestSymbolVisitorConfig)
private const val SmithyVersion = "1.0"
fun String.asSmithy(sourceLocation: String? = null): Model {
fun String.asSmithyModel(sourceLocation: String? = null): Model {
val processed = letIf(!this.startsWith("\$version")) { "\$version: ${SmithyVersion.dq()}\n$it" }
return Model.assembler().discoverModels().addUnparsedModel(sourceLocation ?: "test.smithy", processed).assemble().unwrap()
}