From 6e772b9d609776f39f4dd6ac4f0b26bf46bb8eb8 Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Mon, 2 Nov 2020 14:03:40 -0500 Subject: [PATCH] Generate Builders for Structures (#3) A builder object is generated for Structure shapes. The builder will be fallible (return `Result`) if the structure has required members without defaults. If the structure has no required members, the `build()` method directly returns the constructed object. This required a number of refinements to our module handling as the builders are namespaced to 1-module-per-shape. --- .../smithy/rust/codegen/lang/RustWriter.kt | 70 ++++++++-- .../rust/codegen/smithy/CodegenVisitor.kt | 2 +- .../rust/codegen/smithy/SymbolVisitor.kt | 53 ++++++-- .../smithy/generators/StructureGenerator.kt | 119 ++++++++++++++-- .../amazon/smithy/rust/codegen/util/Exec.kt | 5 +- .../codegen/generators/EnumGeneratorTest.kt | 2 +- .../HttpTraitBindingGeneratorTest.kt | 42 +++--- .../generators/StructureGeneratorTest.kt | 127 +++++++++++------- .../codegen/generators/UnionGeneratorTest.kt | 2 +- .../amazon/smithy/rust/lang/RustWriterTest.kt | 4 +- .../amazon/smithy/rust/testutil/Rust.kt | 24 +++- 11 files changed, 331 insertions(+), 119 deletions(-) diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/lang/RustWriter.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/lang/RustWriter.kt index f5dc76bfe..85ef16cf3 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/lang/RustWriter.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/lang/RustWriter.kt @@ -18,10 +18,19 @@ import software.amazon.smithy.rust.codegen.smithy.isOptional import software.amazon.smithy.rust.codegen.smithy.rustType import software.amazon.smithy.utils.CodeWriter -fun CodeWriter.withBlock(textBeforeNewLine: String, textAfterNewLine: String, block: CodeWriter.() -> Unit): CodeWriter { - openBlock(textBeforeNewLine) +fun CodeWriter.withBlock( + textBeforeNewLine: String, + textAfterNewLine: String, + conditional: Boolean = true, + block: CodeWriter.() -> Unit +): CodeWriter { + if (conditional) { + openBlock(textBeforeNewLine) + } block(this) - closeBlock(textAfterNewLine) + if (conditional) { + closeBlock(textAfterNewLine) + } return this } @@ -35,9 +44,30 @@ fun T.rustBlock(header: String, vararg args: Any, block: T.() - return this } -class RustWriter(filename: String, private val namespace: String, private val commentCharacter: String = "//") : CodegenWriter(null, UseDeclarations(filename, namespace)) { +class RustWriter private constructor(private val filename: String, val namespace: String, private val commentCharacter: String = "//") : + CodegenWriter(null, UseDeclarations(filename, namespace)) { + companion object { + fun forModule(module: String): RustWriter { + return RustWriter("$module.rs", "crate::$module") + } + + val Factory: CodegenWriterFactory = + CodegenWriterFactory { filename, namespace -> + when { + filename.endsWith(".toml") -> RustWriter(filename, namespace, "#") + else -> RustWriter(filename, 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 { putFormatter('T', formatter) } @@ -47,6 +77,23 @@ class RustWriter(filename: String, private val namespace: String, private val co return "${prefix}_$n" } + /** + * Create an inline module. + * [header] should be the declaration of the module, eg. `pub mod Hello`. + * + * The returned writer will inject any local imports into the module as needed. + */ + fun withModule(moduleName: String, visibility: String = "pub", moduleWriter: RustWriter.() -> Unit) { + // 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. + val innerWriter = RustWriter(this.filename, "${this.namespace}::$moduleName") + moduleWriter(innerWriter) + rustBlock("$visibility mod $moduleName") { + write(innerWriter.toString()) + } + } + // TODO: refactor both of these methods & add a parent method to for_each across any field type // generically fun OptionForEach(member: Symbol, outerField: String, block: CodeWriter.(field: String) -> Unit) { @@ -99,7 +146,12 @@ class RustWriter(filename: String, private val namespace: String, private val co is RuntimeType -> { t.dependency?.also { addDependency(it) } // for now, use the fully qualified type name - "::${t.namespace}::${t.name}" + val prefix = if (t.namespace.startsWith("crate")) { + "" + } else { + "::" + } + "$prefix${t.namespace}::${t.name}" } is Symbol -> { if (t.namespace != namespace) { @@ -111,12 +163,4 @@ class RustWriter(filename: String, private val namespace: String, private val co } } } - - companion object { - val Factory: CodegenWriterFactory = - CodegenWriterFactory { filename, namespace -> when { - filename.endsWith(".toml") -> RustWriter(filename, namespace, "#") - else -> RustWriter(filename, namespace) - } } - } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenVisitor.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenVisitor.kt index 41342d155..08b5b1376 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenVisitor.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenVisitor.kt @@ -65,7 +65,7 @@ class CodegenVisitor(context: PluginContext) : ShapeVisitor.Default() { ) cargoToml.render() } - writers.useFileWriter("src/lib.rs") { + writers.useFileWriter("src/lib.rs", "crate::lib") { // TODO: a more structured method of signaling what modules should get loaded. val modules = PublicModules.filter { writers.writers.containsKey("src/$it.rs") } LibRsGenerator(modules, it).render() diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/SymbolVisitor.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/SymbolVisitor.kt index 4e449c1bc..26b2792b4 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/SymbolVisitor.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/SymbolVisitor.kt @@ -69,10 +69,15 @@ fun Symbol.referenceClosure(): List { return listOf(this) + referencedSymbols.flatMap { it.referenceClosure() } } -data class SymbolVisitorConfig(val runtimeConfig: RuntimeConfig, val handleOptionality: Boolean = true, val handleRustBoxing: Boolean = true) +data class SymbolVisitorConfig( + val runtimeConfig: RuntimeConfig, + val handleOptionality: Boolean = true, + val handleRustBoxing: Boolean = true +) // TODO: consider if this is better handled as a wrapper -val DefaultConfig = SymbolVisitorConfig(runtimeConfig = RuntimeConfig(), handleOptionality = true, handleRustBoxing = true) +val DefaultConfig = + SymbolVisitorConfig(runtimeConfig = RuntimeConfig(), handleOptionality = true, handleRustBoxing = true) data class SymbolLocation(val filename: String, val namespace: String) @@ -84,6 +89,19 @@ val Shapes = SymbolLocation("model.rs", "model") val Errors = SymbolLocation("error.rs", "error") val Operations = SymbolLocation("operation.rs", "operation") +fun Symbol.makeOptional(): Symbol { + return if (isOptional()) { + this + } else { + val rustType = RustType.Option(this.rustType()) + Symbol.builder().rustType(rustType) + .rustType(rustType) + .addReference(this) + .name(rustType.name) + .build() + } +} + class SymbolVisitor( private val model: Model, private val rootNamespace: String = "crate", @@ -102,16 +120,12 @@ class SymbolVisitor( } private fun handleOptionality(symbol: Symbol, member: MemberShape, container: Shape): Symbol { - val httpLabeledInput = container.hasTrait(SyntheticInput::class.java) && member.hasTrait(HttpLabelTrait::class.java) + // If a field has the httpLabel trait and we are generating + // an Input shape, then the field is _not optional_. + val httpLabeledInput = + container.hasTrait(SyntheticInput::class.java) && member.hasTrait(HttpLabelTrait::class.java) return if (nullableIndex.isNullable(member) && !httpLabeledInput) { - with(Symbol.builder()) { - val rustType = RustType.Option(symbol.rustType()) - rustType(rustType) - addReference(symbol) - name(rustType.name) - putProperty(SHAPE_KEY, member) - build() - } + symbol.makeOptional() } else symbol } @@ -128,7 +142,7 @@ class SymbolVisitor( } private fun simpleShape(shape: SimpleShape): Symbol { - return symbolBuilder(shape, SimpleShapes.getValue(shape::class)).build() + return symbolBuilder(shape, SimpleShapes.getValue(shape::class)).canUseDefault().build() } override fun booleanShape(shape: BooleanShape): Symbol = simpleShape(shape) @@ -239,7 +253,7 @@ class SymbolVisitor( return builder.rustType(rustType) .name(rustType.name) // Every symbol that actually gets defined somewhere should set a definition file - // If we ever generate a `thisisabug.rs`, we messed something up + // If we ever generate a `thisisabug.rs`, there is a bug in our symbol generation .definitionFile("thisisabug.rs") } } @@ -247,11 +261,24 @@ class SymbolVisitor( // TODO(chore): Move this to a useful place private const val RUST_TYPE_KEY = "rusttype" private const val SHAPE_KEY = "shape" +private const val CAN_USE_DEFAULT = "canusedefault" fun Symbol.Builder.rustType(rustType: RustType): Symbol.Builder { return this.putProperty(RUST_TYPE_KEY, rustType) } +fun Symbol.Builder.canUseDefault(value: Boolean = true): Symbol.Builder { + return this.putProperty(CAN_USE_DEFAULT, value) +} + +/** + * True when it is valid to use the default/0 value for [this] symbol during construction. + */ +fun Symbol.canUseDefault(): Boolean = this.getProperty(CAN_USE_DEFAULT, Boolean::class.javaObjectType).orElse(false) + +/** + * True when [this] is will be represented by Option in Rust + */ fun Symbol.isOptional(): Boolean = when (this.rustType()) { is RustType.Option -> true else -> false diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/StructureGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/StructureGenerator.kt index d210df727..b7297fa09 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/StructureGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/StructureGenerator.kt @@ -10,18 +10,31 @@ import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.MemberShape import software.amazon.smithy.model.shapes.StructureShape 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.render +import software.amazon.smithy.rust.codegen.lang.rustBlock +import software.amazon.smithy.rust.codegen.lang.withBlock +import software.amazon.smithy.rust.codegen.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.smithy.canUseDefault +import software.amazon.smithy.rust.codegen.smithy.isOptional +import software.amazon.smithy.rust.codegen.smithy.makeOptional +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 builders; 1d +// TODO: generate documentation class StructureGenerator( val model: Model, private val symbolProvider: SymbolProvider, private val writer: RustWriter, - private val shape: StructureShape + private val shape: StructureShape, + private val renderBuilder: Boolean = true ) { - private val sortedMembers: List = shape.allMembers.values.sortedBy { symbolProvider.toMemberName(it) } + private val members: List = shape.allMembers.values.toList() + private val structureSymbol = symbolProvider.toSymbol(shape) + private val builderSymbol = RuntimeType("Builder", null, "${structureSymbol.namespace}::${structureSymbol.name.toSnakeCase()}") fun render() { renderStructure() val errorTrait = shape.getTrait(ErrorTrait::class.java) @@ -29,6 +42,12 @@ class StructureGenerator( val errorGenerator = ErrorGenerator(model, symbolProvider, writer, shape, it) errorGenerator.render() } + if (renderBuilder) { + val symbol = symbolProvider.toSymbol(shape) + writer.withModule(symbol.name.toSnakeCase()) { + renderBuilder(this) + } + } } private fun renderStructure() { @@ -36,11 +55,95 @@ class StructureGenerator( // TODO(maybe): Pull derive info from the symbol so that the symbol provider can alter things as necessary; 4h writer.write("#[non_exhaustive]") writer.write("#[derive(Debug, PartialEq, Clone)]") - val blockWriter = writer.openBlock("pub struct ${symbol.name} {") - sortedMembers.forEach { member -> - val memberName = symbolProvider.toMemberName(member) - blockWriter.write("pub $memberName: \$T,", symbolProvider.toSymbol(member)) } - blockWriter.closeBlock("}") + writer.rustBlock("pub struct ${symbol.name}") { + members.forEach { member -> + val memberName = symbolProvider.toMemberName(member) + write("pub $memberName: \$T,", symbolProvider.toSymbol(member)) + } + } + + if (renderBuilder) { + writer.rustBlock("impl ${symbol.name}") { + rustBlock("pub fn builder() -> \$T", builderSymbol) { + write("\$T::default()", builderSymbol) + } + } + } + } + + 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" + writer.write("#[non_exhaustive]") + writer.write("#[derive(Debug, Clone, Default)]") + writer.rustBlock("pub struct $builderName") { + members.forEach { member -> + val memberName = symbolProvider.toMemberName(member) + // All fields in the builder are optional + val memberSymbol = symbolProvider.toSymbol(member).makeOptional() + // TODO: should the builder members be public? + write("$memberName: \$T,", memberSymbol) + } + } + + fun builderConverter(rustType: RustType) = when (rustType) { + is RustType.String -> "inp.into()" + else -> "inp" + } + + writer.rustBlock("impl $builderName") { + members.forEach { member -> + val memberName = symbolProvider.toMemberName(member) + // All fields in the builder are optional + val memberSymbol = symbolProvider.toSymbol(member) + val coreType = memberSymbol.rustType().let { + when (it) { + is RustType.Option -> it.value + else -> it + } + } + val signature = when (coreType) { + is RustType.String -> ">(mut self, inp: T) -> Self" + else -> "(mut self, inp: ${coreType.render()}) -> Self" + } + writer.rustBlock("pub fn $memberName$signature") { + write("self.$memberName = Some(${builderConverter(coreType)});") + write("self") + } + } + + val fallible = members.map { symbolProvider.toSymbol(it) }.any { + // If any members are not optional && we can't use a default, we need to + // generate a fallible builder + !it.isOptional() && !it.canUseDefault() + } + + val returnType = when (fallible) { + true -> "Result<\$T, String>" + false -> "\$T" + } + + writer.rustBlock("pub fn build(self) -> $returnType", structureSymbol) { + withBlock("Ok(", ")", conditional = fallible) { + rustBlock("\$T", structureSymbol) { + members.forEach { member -> + val memberName = symbolProvider.toMemberName(member) + val memberSymbol = symbolProvider.toSymbol(member) + val errorWhenMissing = "$memberName is required when building ${structureSymbol.name}" + val modifier = when { + !memberSymbol.isOptional() && memberSymbol.canUseDefault() -> ".unwrap_or_default()" + !memberSymbol.isOptional() -> ".ok_or(${errorWhenMissing.dq()})?" + else -> "" + } + write("$memberName: self.$memberName$modifier,") + } + } + } + } + } } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/util/Exec.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/util/Exec.kt index 5719adfe0..3dcf0693f 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/util/Exec.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/util/Exec.kt @@ -23,8 +23,9 @@ fun String.runCommand(workdir: Path? = null): String? { proc.waitFor(60, TimeUnit.MINUTES) if (proc.exitValue() != 0) { - val output = proc.errorStream.bufferedReader().readText() - throw CommandFailed("Command Failed\n$output") + val stdErr = proc.errorStream.bufferedReader().readText() + val stdOut = proc.inputStream.bufferedReader().readText() + throw CommandFailed("Command Failed\n$stdErr\n$stdOut") } return proc.inputStream.bufferedReader().readText() } diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/EnumGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/EnumGeneratorTest.kt index 204a8e445..33d4e4d3c 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/EnumGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/EnumGeneratorTest.kt @@ -47,7 +47,7 @@ class EnumGeneratorTest { .assemble() .unwrap() val provider: SymbolProvider = SymbolVisitor(model, "test") - val writer = RustWriter("model.rs", "model") + val writer = RustWriter.forModule("model") val generator = EnumGenerator(provider, writer, shape, trait) generator.render() val result = writer.toString() diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/HttpTraitBindingGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/HttpTraitBindingGeneratorTest.kt index 98ac52dde..986119377 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/HttpTraitBindingGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/HttpTraitBindingGeneratorTest.kt @@ -108,26 +108,20 @@ class HttpTraitBindingGeneratorTest { httpTrait.uriFormatString() shouldBe ("/{bucketName}/{key}".dq()) } - // TODO: when we generate builders, use them to clean up these tests; 1h @Test fun `generate uris`() { - val writer = RustWriter("operation.rs", "operation") + val writer = RustWriter.forModule("operation") // currently rendering the operation renders the protocols—I want to separate that at some point. renderOperation(writer) writer.shouldCompile( """ let ts = Instant::from_epoch_seconds(10123125); - let inp = PutObjectInput { - additional: None, - bucket_name: "somebucket/ok".to_string(), - data: None, - date_header_list: None, - key: ts.clone(), - int_list: None, - extras: Some(vec![0, 1,2,44]), - some_value: Some("svq!!%&".to_string()), - media_type: None - }; + let inp = PutObjectInput::builder() + .bucket_name("somebucket/ok") + .key(ts.clone()) + .extras(vec![0,1,2,44]) + .some_value("svq!!%&") + .build().expect("build should succeed"); let mut o = String::new(); inp.uri_base(&mut o); assert_eq!(o.as_str(), "/somebucket%2Fok/1970-04-28T03:58:45Z"); @@ -140,22 +134,20 @@ class HttpTraitBindingGeneratorTest { @Test fun `build http requests`() { - val writer = RustWriter("operation.rs", "operation") + val writer = RustWriter.forModule("operation") renderOperation(writer) writer.shouldCompile( """ let ts = Instant::from_epoch_seconds(10123125); - let inp = PutObjectInput { - additional: None, - bucket_name: "buk".to_string(), - data: None, - date_header_list: Some(vec![ts.clone()]), - int_list: Some(vec![0,1,44]), - key: Instant::from_epoch_seconds(10123125), - extras: Some(vec![0,1]), - some_value: Some("qp".to_string()), - media_type: Some("base64encodethis".to_string()), - }; + let inp = PutObjectInput::builder() + .bucket_name("buk") + .date_header_list(vec![ts.clone()]) + .int_list(vec![0,1,44]) + .key(ts.clone()) + .extras(vec![0,1]) + .some_value("qp") + .media_type("base64encodethis") + .build().unwrap(); let http_request = inp.build_http_request().body(()).unwrap(); assert_eq!(http_request.uri(), "/buk/1970-04-28T03:58:45Z?paramName=qp&hello=0&hello=1"); assert_eq!(http_request.method(), "PUT"); diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/StructureGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/StructureGeneratorTest.kt index 7eeb072d5..2782b656d 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/StructureGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/StructureGeneratorTest.kt @@ -6,76 +6,111 @@ package software.amazon.smithy.rust.codegen.generators import org.junit.jupiter.api.Test +import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.codegen.core.SymbolProvider -import software.amazon.smithy.model.Model 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.model.traits.DocumentationTrait -import software.amazon.smithy.model.traits.ErrorTrait import software.amazon.smithy.rust.codegen.lang.RustWriter import software.amazon.smithy.rust.codegen.smithy.SymbolVisitor +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.testutil.shouldCompile import software.amazon.smithy.rust.testutil.testSymbolProvider class StructureGeneratorTest { - private val model: Model - private val struct: StructureShape - private val error: StructureShape - init { - val member1 = MemberShape.builder().id("com.test#MyStruct\$foo").target("smithy.api#String").build() - val member2 = MemberShape.builder().id("com.test#MyStruct\$bar").target("smithy.api#PrimitiveInteger").addTrait( - DocumentationTrait("This *is* documentation about the member.") - ).build() - val member3 = MemberShape.builder().id("com.test#MyStruct\$baz").target("smithy.api#Integer").build() - val member4 = MemberShape.builder().id("com.test#MyStruct\$ts").target("smithy.api#Timestamp").build() - - // struct 2 will be of type `Qux` under `MyStruct::quux` member - val struct2 = StructureShape.builder() - .id("com.test#Qux") - .build() - // structure member shape - note the capitalization of the member name (generated code should use the Kotlin class member name) - // val member4 = MemberShape.builder().id("com.test#MyStruct\$Quux").target(struct2).build() - val member5 = MemberShape.builder().id("com.test#MyStruct\$byteValue").target("smithy.api#Byte").build() - - struct = StructureShape.builder() - .id("com.test#MyStruct") - .addMember(member1) - .addMember(member2) - .addMember(member3) - .addMember(member4) - .addMember(member5) - .addTrait(DocumentationTrait("This *is* documentation about the shape.")) - .build() - - val messageMember = MemberShape.builder().id("com.test#MyError\$message").target("smithy.api#String").build() - - error = StructureShape.builder() - .id("com.test#MyError") - .addTrait(ErrorTrait("server")) - .addMember(messageMember).build() - model = Model.assembler() - .addShapes(struct, error, struct2, member1, member2, member3, messageMember) - .assemble() - .unwrap() - } + private val model = """ + namespace com.test + @documentation("this documents the shape") + structure MyStruct { + foo: String, + @documentation("This *is* documentation about the member.") + bar: PrimitiveInteger, + baz: Integer, + ts: Timestamp, + inner: Inner, + byteValue: Byte + } + + // Intentionally empty + structure Inner { + } + + @error("server") + structure MyError { + message: String + } + """.asSmithy() + 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) @Test fun `generate basic structures`() { val provider: SymbolProvider = testSymbolProvider(model) - val writer = RustWriter("model.rs", "model") + val writer = RustWriter.forModule("model") + val innerGenerator = StructureGenerator(model, provider, writer, inner) val generator = StructureGenerator(model, provider, writer, struct) generator.render() + innerGenerator.render() writer.shouldCompile(""" let s: Option = None; s.map(|i|println!("{:?}, {:?}", i.ts, i.byte_value)); - """.trimIndent()) + """.trimIndent() + ) + } + + @Test + fun `generate builders`() { + val provider: SymbolProvider = testSymbolProvider(model) + val writer = RustWriter.forModule("model") + val innerGenerator = StructureGenerator(model, provider, writer, inner) + val generator = StructureGenerator(model, provider, writer, struct) + generator.render() + innerGenerator.render() + writer.shouldCompile( + """ + let my_struct = MyStruct::builder().byte_value(4).foo("hello!").build(); + assert_eq!(my_struct.foo.unwrap(), "hello!"); + assert_eq!(my_struct.bar, 0); + """ + ) + } + + @Test + fun `generate fallible builders`() { + val baseProvider: SymbolProvider = testSymbolProvider(model) + val provider = + object : SymbolProvider { + override fun toSymbol(shape: Shape?): Symbol { + return baseProvider.toSymbol(shape).toBuilder().canUseDefault(false).build() + } + + override fun toMemberName(shape: MemberShape?): String { + return baseProvider.toMemberName(shape) + } + } + val writer = RustWriter.forModule("model") + val innerGenerator = StructureGenerator(model, provider, writer, inner) + val generator = StructureGenerator(model, provider, writer, struct) + generator.render() + innerGenerator.render() + writer.shouldCompile( + """ + let my_struct = MyStruct::builder().byte_value(4).foo("hello!").bar(0).build().expect("required field was not provided"); + assert_eq!(my_struct.foo.unwrap(), "hello!"); + assert_eq!(my_struct.bar, 0); + """ + ) + } @Test fun `generate error structures`() { val provider: SymbolProvider = SymbolVisitor(model, "test") - val writer = RustWriter("errors.rs", "errors") + val writer = RustWriter.forModule("error") val generator = StructureGenerator(model, provider, writer, error) generator.render() writer.shouldCompile() diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/UnionGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/UnionGeneratorTest.kt index 686a57192..0509d520e 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/UnionGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/UnionGeneratorTest.kt @@ -39,7 +39,7 @@ class UnionGeneratorTest { .assemble() .unwrap() val provider: SymbolProvider = SymbolVisitor(model, "test") - val writer = RustWriter("model.rs", "model") + val writer = RustWriter.forModule("model") val generator = UnionGenerator(model, provider, writer, union) generator.render() val result = writer.toString() diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/lang/RustWriterTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/lang/RustWriterTest.kt index 92c7ab59b..a9321bb9e 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/lang/RustWriterTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/lang/RustWriterTest.kt @@ -22,7 +22,7 @@ import software.amazon.smithy.rust.testutil.shouldParseAsRust class RustWriterTest { @Test fun `empty file`() { - val sut = RustWriter("empty.rs", "") + val sut = RustWriter.forModule("empty") sut.toString().shouldParseAsRust() sut.toString().shouldCompile() sut.toString().shouldMatchResource(javaClass, "empty.rs") @@ -30,7 +30,7 @@ class RustWriterTest { @Test fun `manually created struct`() { - val sut = RustWriter("lib.rs", "") + val sut = RustWriter.forModule("lib") val stringShape = StringShape.builder().id("test#Hello").build() val set = SetShape.builder() .id("foo.bar#Records") diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/testutil/Rust.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/testutil/Rust.kt index 32f6d29f3..91907000d 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/testutil/Rust.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/testutil/Rust.kt @@ -21,7 +21,7 @@ fun String.shouldParseAsRust() { fun RustWriter.shouldCompile(main: String = "") { val deps = this.dependencies.map { RustDependency.fromSymbolDependency(it) } try { - this.toString().shouldCompile(deps.toSet(), main) + this.toString().shouldCompile(deps.toSet(), module = this.namespace.split("::")[1], main = main) } catch (e: CommandFailed) { // When the test fails, print the code for convenience println(this.toString()) @@ -29,7 +29,7 @@ fun RustWriter.shouldCompile(main: String = "") { } } -fun String.shouldCompile(deps: Set, main: String = "") { +fun String.shouldCompile(deps: Set, module: String? = null, main: String = "") { this.shouldParseAsRust() val tempDir = createTempDir() // TODO: unify this with CargoTomlGenerator @@ -46,13 +46,23 @@ fun String.shouldCompile(deps: Set, main: String = "") { tempDir.resolve("Cargo.toml").writeText(cargoToml) tempDir.resolve("src").mkdirs() val mainRs = tempDir.resolve("src/main.rs") - mainRs.writeText(this) - if (!this.contains("fn main")) { - mainRs.appendText("\nfn main() { $main }\n") - } + val testModule = tempDir.resolve("src/$module.rs") + testModule.writeText(this) + testModule.appendText(""" + #[test] + fn test() { + $main + } + """.trimIndent()) + mainRs.appendText(""" + pub mod $module; + use crate::$module::*; + fn main() { + } + """.trimIndent()) "cargo check".runCommand(tempDir.toPath()) if (main != "") { - "cargo run".runCommand(tempDir.toPath()) + "cargo test".runCommand(tempDir.toPath()) } } -- GitLab