├── 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 | }
--------------------------------------------------------------------------------