├── project └── build.properties ├── .travis.yml ├── .gitignore ├── LICENSE.txt ├── core └── src │ ├── main │ └── scala │ │ └── com │ │ └── github │ │ └── harveywi │ │ └── builder │ │ ├── FieldsContainer.scala │ │ ├── HasBuilderParams.scala │ │ └── HasBuilder.scala │ └── test │ └── scala │ └── com │ └── github │ └── harveywi │ └── builder │ └── HasBuilderSpec.scala ├── examples └── src │ └── main │ └── scala │ └── com │ └── github │ └── harveywi │ └── builder │ └── examples │ └── OrderOfScotch.scala └── README.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.3.8 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | 3 | scala: 4 | - 2.10.7 5 | - 2.11.12 6 | - 2.12.10 7 | - 2.13.1 8 | 9 | jdk: 10 | - openjdk8 11 | 12 | script: 13 | - sbt test 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | 3 | *.class 4 | *.log 5 | 6 | # sbt specific 7 | .cache 8 | .cache-main 9 | .cache-tests 10 | .classpath 11 | .project 12 | .settings/* 13 | core/.settings/* 14 | examples/.settings/* 15 | .history 16 | .lib/ 17 | .directory 18 | dist/* 19 | target/ 20 | lib_managed/ 21 | src_managed/ 22 | project/boot/ 23 | project/plugins/project/ 24 | 25 | # Scala-IDE specific 26 | .scala_dependencies 27 | .worksheet 28 | 29 | # IntelliJ IDEA 30 | *.iml 31 | .idea/ 32 | *.ipr 33 | *.iws 34 | /out/ 35 | .idea_modules/ 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | shapeless-builder 2 | Copyright (c) William Harvey 3 | All rights reserved. 4 | MIT License 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ""Software""), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /core/src/main/scala/com/github/harveywi/builder/FieldsContainer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * shapeless-builder 3 | * (c) William Harvey 2013 4 | * harveywi@cse.ohio-state.edu 5 | * 6 | * This file is part of "shapeless-builder". 7 | * 8 | * shapeless-builder is free software: you can redistribute it and/or modify 9 | * it under the terms of the GNU General Public License as published by 10 | * the Free Software Foundation, either version 3 of the License, or 11 | * (at your option) any later version. 12 | * 13 | * shapeless-builder is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | * GNU General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU General Public License 19 | * along with shapeless-builder. If not, see . 20 | */ 21 | 22 | package com.github.harveywi.builder 23 | 24 | import shapeless._ 25 | 26 | /** 27 | * A container storing an `HList` of builder parameters (corresponding to case class constructor parameters/fields). 28 | * This is sort of a hack which helps library users to avoid manually specifying the type signature of the HList, 29 | * which can become unruly in practice. 30 | * 31 | * @author William Harvey 32 | */ 33 | trait FieldsContainer { 34 | type L <: HList 35 | def fields: L 36 | } -------------------------------------------------------------------------------- /core/src/main/scala/com/github/harveywi/builder/HasBuilderParams.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * shapeless-builder 3 | * (c) William Harvey 2013 4 | * harveywi@cse.ohio-state.edu 5 | * 6 | * This file is part of "shapeless-builder". 7 | * 8 | * shapeless-builder is free software: you can redistribute it and/or modify 9 | * it under the terms of the GNU General Public License as published by 10 | * the Free Software Foundation, either version 3 of the License, or 11 | * (at your option) any later version. 12 | * 13 | * shapeless-builder is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | * GNU General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU General Public License 19 | * along with shapeless-builder. If not, see . 20 | */ 21 | 22 | package com.github.harveywi.builder 23 | 24 | /** 25 | * Contains types representing the required and optional parameters needed to manufacture 26 | * instances of case classes using the builder pattern. 27 | * 28 | * @author William Harvey 29 | */ 30 | trait HasBuilderParams {self: HasBuilder[_] => 31 | /** 32 | * A required parameter. 33 | */ 34 | trait Param[+T] 35 | 36 | /** 37 | * A parameter whose value has been populated. 38 | */ 39 | class PParam[+T](val value: T) extends Param[T] 40 | 41 | /** 42 | * An optional parameter. 43 | */ 44 | abstract class OptParam[+T](override val value: T) extends PParam[T](value) 45 | } -------------------------------------------------------------------------------- /examples/src/main/scala/com/github/harveywi/builder/examples/OrderOfScotch.scala: -------------------------------------------------------------------------------- 1 | package com.github.harveywi.builder.examples 2 | 3 | import com.github.harveywi.builder.HasBuilder 4 | import shapeless._ 5 | 6 | /** 7 | * Example demonstrating the builder pattern for a shot of scotch (see 8 | * http://blog.rafaelferreira.net/2008/07/type-safe-builder-pattern-in-scala.html). 9 | * 10 | * Huge thanks to Rafael Ferreira for providing this use case! 11 | * 12 | * @author William Harvey 13 | */ 14 | object OrderOfScotch { 15 | 16 | sealed abstract class Preparation 17 | case object Neat extends Preparation 18 | case object OnTheRocks extends Preparation 19 | case object WithWater extends Preparation 20 | 21 | sealed abstract class Glass 22 | case object Short extends Glass 23 | case object Tall extends Glass 24 | case object Tulip extends Glass 25 | 26 | case class OrderOfScotch( 27 | brand: String, 28 | mode: Preparation, 29 | isDouble: Boolean, 30 | glass: Option[Glass]) 31 | object OrderOfScotch extends HasBuilder[OrderOfScotch] { 32 | object Brand extends Param[String] 33 | object Mode extends Param[Preparation] 34 | object IsDouble extends Param[Boolean] 35 | object Glass extends OptParam[Option[Glass]](None) 36 | 37 | // Establish HList <=> OrderOfScotch isomorphism 38 | val gen = Generic[OrderOfScotch] 39 | // Establish Param[_] <=> constructor parameter correspondence 40 | val fieldsContainer = createFieldsContainer(Brand :: Mode :: IsDouble :: Glass :: HNil) 41 | // That's all! 42 | } 43 | 44 | def main(args: Array[String]): Unit = { 45 | import OrderOfScotch._ 46 | 47 | val order1 = OrderOfScotch.builder.set(Brand, "Takes").set(IsDouble, true). 48 | set(Glass, Some(Tall)).set(Mode, OnTheRocks).build() 49 | 50 | // Point-free version of the above 51 | val order2 = (OrderOfScotch.builder 52 | set (Brand, "Takes") 53 | set (IsDouble, true) 54 | set (Glass, Some(Tall)) 55 | set (Mode, OnTheRocks) 56 | build ()) 57 | 58 | assert(order1 == OrderOfScotch("Takes", OnTheRocks, isDouble = true, Some(Tall)), 59 | "Time to get out the scotch...") 60 | 61 | assert(order1 == order2, "Traditional and point-free build results should be identical") 62 | } 63 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | shapeless-builder 2 | ======================= 3 | 4 | shapeless-builder is a [Scala](http://www.scala-lang.org) library which 5 | takes advantage of [Miles Sabin's](https://github.com/milessabin) 6 | [shapeless](https://github.com/milessabin/shapeless) library to endow case classes 7 | with method-chaining builders (essentially the builder design pattern). These 8 | builders are type-safe and purely functional. The Scala type system is used to ensure that 9 | code attempting to build incomplete objects will not compile. 10 | 11 | This work was inspired by a [blog post](http://blog.rafaelferreira.net/2008/07/type-safe-builder-pattern-in-scala.html) 12 | by [Rafael Ferreira](http://blog.rafaelferreira.net/). Thanks Rafael! 13 | 14 | The isomorphism between case classes and HLists and generous use of implicits 15 | are the driving forces behind shapeless-builder. 16 | 17 | This library is one part in a trilogy ([I. shapeless-serialization](https://github.com/harveywi/shapeless-serialization), 18 | [II. shapeless-builder](), III. shapeless-commandline (coming soon!)) of shapeless-based libraries that I recently cooked up 19 | to both deepen my understanding of Scala and to scratch some technical itches. 20 | I hope you find it useful and interesting! 21 | 22 | Example 1 23 | -------------------------------- 24 | 25 | ```scala 26 | // Define a case class 27 | case class Foo(x: Int, y: String, z: Char) 28 | 29 | // Mix the HasBuilder trait in with its companion object 30 | object Foo extends HasBuilder[Foo] { 31 | // Establish the case class <=> HList isomorphism 32 | val isoContainer = createIsoContainer(apply _, unapply _) 33 | 34 | // Define objects corresponding to the case class constructor parameters: 35 | // X is a required parameter of type Int 36 | object X extends Param[Int] 37 | 38 | // Y is an optional parameter of type String with default value "5" 39 | object Y extends OptParam[String]("5") 40 | 41 | // Z is an optional parameter of type Char with default value '5' 42 | object Z extends OptParam[Char]('5') 43 | 44 | // Define the "fieldsContainer" by passing in an HList of the above objects. The order of the 45 | // objects in the HList must correspond to the order of the case class constructor parameters. 46 | val fieldsContainer = createFieldsContainer(X :: Y :: Z :: HNil) 47 | } 48 | 49 | // [...] 50 | 51 | // Now you can create instances of the case class by using method-chaining builder incantations 52 | import Foo._ 53 | val foo = Foo.builder.set(X, 42).set(Z, '#').build() 54 | 55 | // Yessssssss! 56 | assert(foo == Foo(42, "5", '#'), "Nooooooooooo!") 57 | ``` 58 | 59 | Example 2 60 | -------------------------------- 61 | 62 | ```scala 63 | import com.github.harveywi.builder.HasBuilder 64 | import shapeless._ 65 | 66 | /** 67 | * Example demonstrating the builder pattern for a shot of scotch (see 68 | * http://blog.rafaelferreira.net/2008/07/type-safe-builder-pattern-in-scala.html). 69 | * 70 | * Huge thanks to Rafael Ferreira for providing this use case! 71 | * 72 | * @author William Harvey 73 | */ 74 | object OrderOfScotch { 75 | 76 | sealed abstract class Preparation 77 | case object Neat extends Preparation 78 | case object OnTheRocks extends Preparation 79 | case object WithWater extends Preparation 80 | 81 | sealed abstract class Glass 82 | case object Short extends Glass 83 | case object Tall extends Glass 84 | case object Tulip extends Glass 85 | 86 | case class OrderOfScotch( 87 | brand: String, 88 | mode: Preparation, 89 | isDouble: Boolean, 90 | glass: Option[Glass]) 91 | 92 | object OrderOfScotch extends HasBuilder[OrderOfScotch] { 93 | object Brand extends Param[String] 94 | object Mode extends Param[Preparation] 95 | object IsDouble extends Param[Boolean] 96 | object Glass extends OptParam[Option[Glass]](None) 97 | 98 | // Establish HList <=> OrderOfScotch isomorphism 99 | val isoContainer = createIsoContainer(apply _, unapply _) 100 | // Establish Param[_] <=> constructor parameter correspondence 101 | val fieldsContainer = createFieldsContainer(Brand :: Mode :: IsDouble :: Glass :: HNil) 102 | // That's all! 103 | } 104 | 105 | def main(args: Array[String]): Unit = { 106 | import OrderOfScotch._ 107 | 108 | val order1 = OrderOfScotch.builder.set(Brand, "Takes").set(IsDouble, true). 109 | set(Glass, Some(Tall)).set(Mode, OnTheRocks).build() 110 | 111 | // Point-free version of the above 112 | val order2 = (OrderOfScotch.builder 113 | set(Brand, "Takes") 114 | set(IsDouble, true) 115 | set(Glass, Some(Tall)) 116 | set(Mode, OnTheRocks) 117 | build()) 118 | 119 | assert(order1 == OrderOfScotch("Takes", OnTheRocks, true, Some(Tall)), 120 | "Time to get out the scotch...") 121 | 122 | assert(order1 == order2, "Traditional and point-free build results should be identical") 123 | } 124 | } 125 | ``` 126 | 127 | For more examples, see the test specifications [here](https://github.com/harveywi/shapeless-builder/tree/master/src/test/com/github/harveywi/builder). 128 | 129 | Prerequisites 130 | -------------------------------- 131 | This library requires Scala 2.10 and shapeless 1.2.3. 132 | 133 | Scaladoc 134 | -------------------------------- 135 | Scaladoc is available [here](http://www.aylasoftware.org/shapeless-builder/). 136 | 137 | ### Questions? Comments? Bugs? 138 | Feel free to contact me (harveywi at cse dot ohio-state dot edu). Thanks! 139 | 140 | -------------------------------------------------------------------------------- /core/src/main/scala/com/github/harveywi/builder/HasBuilder.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * shapeless-builder 3 | * (c) William Harvey 2013 4 | * harveywi@cse.ohio-state.edu 5 | * 6 | * This file is part of "shapeless-builder". 7 | * 8 | * shapeless-builder is free software: you can redistribute it and/or modify 9 | * it under the terms of the GNU General Public License as published by 10 | * the Free Software Foundation, either version 3 of the License, or 11 | * (at your option) any later version. 12 | * 13 | * shapeless-builder is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | * GNU General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU General Public License 19 | * along with shapeless-builder. If not, see . 20 | */ 21 | 22 | package com.github.harveywi.builder 23 | 24 | import shapeless._ 25 | import shapeless.ops.hlist._ 26 | 27 | /** 28 | * Enables method-chaining builders for case classes of type `CC`. 29 | * 30 | * @example {{{ 31 | * 32 | * // Define a case class 33 | * case class Foo(x: Int, y: String, z: Char) 34 | * 35 | * // Mix the HasBuilder trait in with its companion object 36 | * object Foo extends HasBuilder[Foo] { 37 | * // Establish the case class <=> HList isomorphism 38 | * val isoContainer = createIsoContainer(apply _, unapply _) 39 | * 40 | * // Define objects corresponding to the case class constructor parameters: 41 | * // X is a required parameter of type Int 42 | * object X extends Param[Int](5) 43 | * 44 | * // Y is an optional parameter of type String with default value "5" 45 | * object Y extends OptParam[String]("5") 46 | * 47 | * // Z is an optional parameter of type Char with default value '5' 48 | * object Z extends OptParam[Char]('5') 49 | * 50 | * // Define the "fieldsContainer" by passing in an HList of the above objects. The order of the 51 | * // objects in the HList must correspond to the order of the case class constructor parameters. 52 | * val fieldsContainer = createFieldsContainer(X :: Y :: Z :: HNil) 53 | * } 54 | * 55 | * // [...] 56 | * 57 | * // Now you can create instances of the case class by using method-chaining builder incantations 58 | * import Foo._ 59 | * val test = Foo.builder.set(X, 42).set(Z, '#').build() 60 | * 61 | * // Yessssssss! 62 | * assert(foo == Foo(42, "5", '#'), "Nooooooooooo!") 63 | * }}} 64 | * 65 | * @author William Harvey 66 | */ 67 | trait HasBuilder[CC] extends HasBuilderParams { 68 | 69 | trait ParamValueExtractor[In <: HList, Out <: HList] { 70 | def apply(in: In): Out 71 | } 72 | 73 | object ParamValueExtractor { 74 | implicit def caseHNil: ParamValueExtractor[HNil, HNil] = 75 | HNil => HNil 76 | 77 | implicit def casePParam[T, O, L1 <: HList, L2 <: HList]( 78 | implicit ev: O <:< PParam[T], 79 | tailExtractor: ParamValueExtractor[L1, L2] 80 | ): ParamValueExtractor[O :: L1, T :: L2] = 81 | (in: O :: L1) => in.head.value :: tailExtractor(in.tail) 82 | } 83 | 84 | val gen: Generic[CC] { 85 | type Repr <: HList 86 | } 87 | 88 | /** 89 | * Establishes a correspondence between the `Param[_]`/`OptParam[_]` objects and the constructor parameters for `CC`. 90 | * The ordering of the `Param[_]`/`OptParam[_]` objects must mimic the ordering of their corresponding constructor parameters. 91 | * 92 | * The returned object is just a wrapper for an `HList`; using a wrapper allows us to tuck away the type signature of the `HList` 93 | * as a type variable. 94 | * 95 | * @param fieldsIn an HList of `Param[_]`/`OptParam[_]` objects ordered in accordance with the constructor parameters for `CC` 96 | * @return a `FieldsContainer` representing the constructor parameters for `CC` 97 | */ 98 | def createFieldsContainer[L1 <: HList](fieldsIn: L1)(implicit lubConstraint: LUBConstraint[L1, Param[_]]): FieldsContainer { type L = L1 } = 99 | new FieldsContainer { 100 | type L = L1 101 | def fields: L = fieldsIn 102 | } 103 | 104 | val fieldsContainer: FieldsContainer 105 | 106 | /** 107 | * Creates a new builder for instances of `CC`. 108 | */ 109 | def builder = new Builder(fieldsContainer.fields) 110 | 111 | /** 112 | * A builder for instances of `CC`. 113 | */ 114 | class Builder[L <: HList](val fields: L) { 115 | /** 116 | * Set the value of the constructor parameter `key` to the specified value `value`. If you already suppled a value for parameter `key`, 117 | * then attempting to set a different value for `key` will result in a compiler error. 118 | * 119 | * @param key constructor parameter to set 120 | * @param value value used to inhabit the specified constructor parameter `key` 121 | * @return a new builder instance with the value of parameter `key` set to `value` 122 | */ 123 | def set[V, K, T, Out <: HList](key: K, value: V)(implicit ev1: K <:< Param[T], ev2: V <:< T, selector: Selector[L, K], 124 | replacer: Replacer.Aux[L, K, PParam[V], (K, Out)]): Builder[Out] = { 125 | val newValue = new PParam[V](value) 126 | val newParams = replacer(fields, newValue)._2 127 | new Builder(newParams) 128 | } 129 | 130 | def build() 131 | (implicit 132 | paramValueExtractor: ParamValueExtractor[L, gen.Repr] 133 | ) 134 | : CC = { 135 | val params = paramValueExtractor(fields) 136 | gen.from(params) 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /core/src/test/scala/com/github/harveywi/builder/HasBuilderSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * shapeless-builder 3 | * (c) William Harvey 2013 4 | * harveywi@cse.ohio-state.edu 5 | * 6 | * This file is part of "shapeless-builder". 7 | * 8 | * shapeless-builder is free software: you can redistribute it and/or modify 9 | * it under the terms of the GNU General Public License as published by 10 | * the Free Software Foundation, either version 3 of the License, or 11 | * (at your option) any later version. 12 | * 13 | * shapeless-builder is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | * GNU General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU General Public License 19 | * along with shapeless-builder. If not, see . 20 | */ 21 | 22 | package com.github.harveywi.builder 23 | 24 | import org.scalatest.flatspec.AnyFlatSpec 25 | import org.scalatest.matchers.should.Matchers 26 | import shapeless._ 27 | 28 | object HasBuilderSpec { 29 | // Simple single-parameter tests 30 | case class TestInt(x: Int) 31 | object TestInt extends HasBuilder[TestInt] { 32 | val gen = Generic[TestInt] 33 | 34 | object X extends Param[Int] 35 | val fieldsContainer = createFieldsContainer(X :: HNil) 36 | } 37 | 38 | case class TestIntOptional(x: Int) 39 | object TestIntOptional extends HasBuilder[TestIntOptional] { 40 | val gen = Generic[TestIntOptional] 41 | 42 | object X extends OptParam[Int](8675309) 43 | val fieldsContainer = createFieldsContainer(X :: HNil) 44 | } 45 | 46 | // Multiple parameters test 47 | case class TestIntStringChar(x: Int, y: String, z: Char) 48 | object TestIntStringChar extends HasBuilder[TestIntStringChar] { 49 | val gen = Generic[TestIntStringChar] 50 | 51 | object X extends Param[Int] 52 | object Y extends Param[String] 53 | object Z extends Param[Char] 54 | val fieldsContainer = createFieldsContainer(X :: Y :: Z :: HNil) 55 | } 56 | 57 | // Multiple optional parameters 58 | case class TestIntStringCharOptional(x: Int, y: String, z: Char) 59 | object TestIntStringCharOptional extends HasBuilder[TestIntStringCharOptional] { 60 | val gen = Generic[TestIntStringCharOptional] 61 | 62 | object X extends OptParam[Int](5) 63 | object Y extends OptParam[String]("5") 64 | object Z extends OptParam[Char]('5') 65 | val fieldsContainer = createFieldsContainer(X :: Y :: Z :: HNil) 66 | } 67 | 68 | // Fun with Option[_]... 69 | case class TestOptionString(x: Option[String]) 70 | object TestOptionString extends HasBuilder[TestOptionString] { 71 | val gen = Generic[TestOptionString] 72 | 73 | object X extends Param[Option[String]] 74 | val fieldsContainer = createFieldsContainer(X :: HNil) 75 | } 76 | 77 | case class TestOptionStringOptional(x: Option[String]) 78 | object TestOptionStringOptional extends HasBuilder[TestOptionStringOptional] { 79 | val gen = Generic[TestOptionStringOptional] 80 | 81 | object X extends OptParam[Option[String]](None) 82 | val fieldsContainer = createFieldsContainer(X :: HNil) 83 | } 84 | 85 | // Some more fun with Either[_] and Option[_] 86 | case class TestEither(x: Either[Int, String], y: Option[Either[Int, String]]) 87 | object TestEither extends HasBuilder[TestEither] { 88 | val gen = Generic[TestEither] 89 | 90 | object X extends Param[Either[Int, String]] 91 | object Y extends OptParam[Option[Either[Int, String]]](None) 92 | val fieldsContainer = createFieldsContainer(X :: Y :: HNil) 93 | } 94 | } 95 | 96 | class HasBuilderSpec extends AnyFlatSpec with Matchers { 97 | import HasBuilderSpec._ 98 | "When method chaining is not used, a builder for a case class with a single required parameter" should "generate the expected case class" in { 99 | val expected = TestInt(42) 100 | 101 | import TestInt._ 102 | 103 | // Create a new builder for instances of TestInt 104 | val builder = TestInt.builder 105 | 106 | // The commented line below does not compile because the build() method needs a value for X 107 | // builder.build() 108 | 109 | // Set the value of parameter x 110 | val builderWithX = builder.set(X, 42) 111 | 112 | // The commented line below does not compile because, by design, you can only set each parameter value once 113 | // val builderWithXX = builderWithX.set(X, 100) 114 | 115 | // Once all the required arguments have been supplied, the build() method is ready to go 116 | val builderResult = builderWithX.build() 117 | 118 | builderResult should equal(expected) 119 | } 120 | 121 | "When method chaining is used, a builder for a case class with a single required parameter" should "generate the expected case class" in { 122 | import TestInt._ 123 | val expected = TestInt(42) 124 | 125 | // Method chaining can be more succinct 126 | TestInt.builder.set(X, 42).build() should equal(expected) 127 | 128 | // Chaining using point-free style 129 | TestInt.builder set (X, 42) build () should equal(expected) 130 | } 131 | 132 | // Try out the optional parameters 133 | "A builder for a case class with a single optional parameter" should "correctly populate the default parameter value" in { 134 | val expected = TestIntOptional(8675309) 135 | TestIntOptional.builder.build() should equal(expected) 136 | } 137 | 138 | "A builder for a case class with a single optional parameter" should "correctly accept a user-specified parameter value" in { 139 | import TestIntOptional._ 140 | val expected = TestIntOptional(42) 141 | TestIntOptional.builder.set(X, 42).build() should equal(expected) 142 | } 143 | 144 | // Test multiple (required) parameters 145 | "A builder for a case class with multiple required parameters" should "generate the expected case class" in { 146 | import TestIntStringChar._ 147 | val expected = TestIntStringChar(42, "Peanuts", 'E') 148 | 149 | // Let's try all six permutations for fun 150 | val builder = TestIntStringChar.builder 151 | val x = 42 152 | val y = "Peanuts" 153 | val z = 'E' 154 | builder set (X, x) set (Y, y) set (Z, z) build () should equal(expected) 155 | builder set (X, x) set (Z, z) set (Y, y) build () should equal(expected) 156 | builder set (Y, y) set (X, x) set (Z, z) build () should equal(expected) 157 | builder set (Y, y) set (Z, z) set (X, x) build () should equal(expected) 158 | builder set (Z, z) set (X, x) set (Y, y) build () should equal(expected) 159 | builder set (Z, z) set (Y, y) set (X, x) build () should equal(expected) 160 | } 161 | 162 | // Test multiple optional parameters 163 | "A builder for a case class with multiple optional parameters" should "generate the expected case class" in { 164 | import TestIntStringCharOptional._ 165 | 166 | val builder = TestIntStringCharOptional.builder 167 | 168 | // Default values 169 | val dx = 5 170 | val dy = "5" 171 | val dz = '5' 172 | 173 | // Non-default values 174 | val x = 42 175 | val y = "Peanuts" 176 | val z = '#' 177 | 178 | // Just a quickie enrichment to make some of the below tests more concise 179 | implicit class EnrichmentForTests(expected: TestIntStringCharOptional) { 180 | def apply(op: TestIntStringCharOptional => Unit) = op(expected) 181 | } 182 | 183 | // No arguments omitted 184 | TestIntStringCharOptional(x, y, z) { expected => 185 | builder set (X, x) set (Y, y) set (Z, z) build () should equal(expected) 186 | builder set (X, x) set (Z, z) set (Y, y) build () should equal(expected) 187 | builder set (Y, y) set (X, x) set (Z, z) build () should equal(expected) 188 | builder set (Y, y) set (Z, z) set (X, x) build () should equal(expected) 189 | builder set (Z, z) set (X, x) set (Y, y) build () should equal(expected) 190 | builder set (Z, z) set (Y, y) set (X, x) build () should equal(expected) 191 | } 192 | 193 | // x argument omitted 194 | TestIntStringCharOptional(dx, y, z) { expected => 195 | builder set (Y, y) set (Z, z) build () should equal(expected) 196 | builder set (Z, z) set (Y, y) build () should equal(expected) 197 | } 198 | 199 | // y argument omitted 200 | TestIntStringCharOptional(x, dy, z) { expected => 201 | builder set (X, x) set (Z, z) build () should equal(expected) 202 | builder set (Z, z) set (X, x) build () should equal(expected) 203 | } 204 | 205 | // z argument omitted 206 | TestIntStringCharOptional(x, y, dz) { expected => 207 | builder set (X, x) set (Y, y) build () should equal(expected) 208 | builder set (Y, y) set (X, x) build () should equal(expected) 209 | } 210 | 211 | // x and y omitted 212 | TestIntStringCharOptional(dx, dy, z) { expected => 213 | builder set (Z, z) build () should equal(expected) 214 | } 215 | 216 | // x and z omitted 217 | TestIntStringCharOptional(dx, y, dz) { expected => 218 | builder set (Y, y) build () should equal(expected) 219 | } 220 | 221 | // y and z omitted 222 | TestIntStringCharOptional(x, dy, dz) { expected => 223 | builder set (X, x) build () should equal(expected) 224 | } 225 | 226 | // All arguments omitted 227 | TestIntStringCharOptional(dx, dy, dz) { expected => 228 | builder.build() should equal(expected) 229 | } 230 | } 231 | 232 | "A builder for a case class with a complex parameter" should "generate the expected case class" in { 233 | import TestOptionString._ 234 | val builder = TestOptionString.builder 235 | builder.set(X, None).build() should equal(TestOptionString(None)) 236 | builder.set(X, Some("Hello")).build() should equal(TestOptionString(Some("Hello"))) 237 | builder.set(X, Some("Hello")).build() should not equal(TestOptionString(Some("asdf"))) 238 | } 239 | 240 | "A builder for a case class with a complex optional parameter" should "generate the expected case class" in { 241 | import TestOptionStringOptional._ 242 | val builder = TestOptionStringOptional.builder 243 | 244 | // No parameters 245 | builder.build() should equal(TestOptionStringOptional(None)) 246 | 247 | // Set to `None` 248 | builder.set(X, None).build() should equal(TestOptionStringOptional(None)) 249 | 250 | // Set to `Some(...)` 251 | builder.set(X, Some("Hello")).build() should equal(TestOptionStringOptional(Some("Hello"))) 252 | } 253 | 254 | "A builder challenged with a conundrum involving Either and Option" should "generate the expected case classes" in { 255 | import TestEither._ 256 | val builder = TestEither.builder 257 | 258 | TestEither(Left(42), None) should equal { 259 | builder set(X, Left(42)) build() 260 | } 261 | 262 | TestEither(Right("Hello"), None) should equal { 263 | builder set(X, Right("Hello")) build() 264 | } 265 | 266 | TestEither(Left(42), Some(Left(1000))) should equal { 267 | builder set(Y, Some(Left(1000))) set(X, Left(42)) build() 268 | } 269 | 270 | TestEither(Left(42), Some(Right("World"))) should equal { 271 | builder set(Y, Some(Right("World"))) set(X, Left(42)) build() 272 | } 273 | 274 | TestEither(Right("Hello"), Some(Left(1000))) should equal { 275 | builder set(Y, Some(Left(1000))) set(X, Right("Hello")) build() 276 | } 277 | 278 | TestEither(Right("Hello"), Some(Right("World"))) should equal { 279 | builder set(Y, Some(Right("World"))) set(X, Right("Hello")) build() 280 | } 281 | } 282 | } --------------------------------------------------------------------------------