├── .gitignore ├── CHANGELOG ├── README.md ├── automapper-macros └── src │ └── main │ └── scala │ └── io │ └── bfil │ └── automapper │ └── Mapping.scala ├── automapper └── src │ ├── main │ └── scala │ │ └── io │ │ └── bfil │ │ └── automapper │ │ └── package.scala │ └── test │ └── scala │ └── io │ └── bfil │ └── automapper │ └── AutomapperSpec.scala ├── build.sbt ├── project └── build.properties └── version.sbt /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | Version 0.7.0 (2019-08-29) 2 | -------------------------- 3 | - release for Scala 2.13 4 | 5 | 6 | Version 0.6.2 (2019-08-29) 7 | -------------------------- 8 | - fixed type mismatch errors caused by unknown Scala compiler internals 9 | 10 | 11 | Version 0.6.1 (2017-12-22) 12 | -------------------------- 13 | - using the fully qualified Mapping trait in the macro to avoid possible clashes 14 | 15 | 16 | Version 0.6.0 (2017-06-05) 17 | -------------------------- 18 | - renamed organization and packages from com.bfil to io.bfil 19 | - published the library to Bintray 20 | 21 | 22 | Version 0.5.0 (2017-06-01) 23 | -------------------------- 24 | - fixed an issue related to incremental compilation 25 | 26 | 27 | Version 0.4.0 (2017-01-13) 28 | -------------------------- 29 | - added Scala 2.12 support 30 | - removed AutoMapping and DynamicMapping, import io.bfil.automapper._ instead 31 | - simplified API to: automap(source).to[Target] and automap(source).dynamicallyTo[Target](dynamicField = value) 32 | - improved type safety at compile time for dynamic mappings 33 | - removed Dynamic lookup runtime overhead by pushing it to the macro at compile time 34 | - switched tests to scalatest to support testing for compilation errors 35 | 36 | 37 | Version 0.3.0 (2015-09-25) 38 | -------------------------- 39 | - fixed issues with default values 40 | - giving dynamic mappings precedence over everything else 41 | - bug fix to only use dynamic mapping at the root level when generating the mapping 42 | - minor API changes 43 | 44 | 45 | Version 0.2.0 (2015-09-24) 46 | -------------------------- 47 | - added support for Iterable Map fields mapping (only Map values, not keys) 48 | - added DynamicMapping to enable dynamic field mapping 49 | 50 | 51 | Version 0.1.0 (2015-09-16) 52 | -------------------------- 53 | first public release 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Scala AutoMapper 2 | ================ 3 | 4 | A library that uses macros to generate mappings between case classes. 5 | 6 | #### Current features 7 | 8 | - Nested case classes 9 | - Optional fields 10 | - Iterable fields 11 | - Map fields (only values) 12 | - Default values 13 | - Compile time errors for incomplete mappings 14 | - Dynamic field mapping 15 | - Polymorphic types mapping (using implicit conversions) 16 | 17 | #### Planned features 18 | 19 | - Map keys mapping 20 | 21 | *Anything else you would like to see here? Feel free to open an issue or contribute!* 22 | 23 | Setting up the dependencies 24 | --------------------------- 25 | 26 | __Scala AutoMapper__ is available on `Maven Central` (since version `0.6.0`). 27 | 28 | For **Scala 2.13** use version `0.7.0`. 29 | 30 | For **Scala 2.12** and **Scala 2.11** use version `0.6.2`. 31 | 32 | Using SBT, add the following dependency to your build file: 33 | 34 | ```scala 35 | libraryDependencies ++= Seq( 36 | "io.bfil" %% "automapper" % "0.7.0" 37 | ) 38 | ``` 39 | 40 | If you have issues resolving the dependency, you can add the following resolver: 41 | 42 | ```scala 43 | resolvers += Resolver.bintrayRepo("bfil", "maven") 44 | ``` 45 | 46 | Usage 47 | ----- 48 | 49 | Let's use the following classes for a very simple example: 50 | 51 | ```scala 52 | case class SourceClass(label: String, value: Int) 53 | case class TargetClass(label: String, value: Int) 54 | ``` 55 | 56 | To map a source class instance to the target class use any of the following ways: 57 | 58 | ```scala 59 | import io.bfil.automapper._ 60 | 61 | val source = SourceClass("label", 10) 62 | val target = automap(source).to[TargetClass] 63 | ``` 64 | 65 | ### Using implicit mappings 66 | 67 | Implicit mappings can be defined separately and then used to map case classes 68 | 69 | ```scala 70 | import io.bfil.automapper._ 71 | 72 | val source = SourceClass("label", 10) 73 | 74 | trait MyMappings { 75 | implicit val mapping1 = generateMapping[SourceClass, TargetClass] 76 | implicit val mapping2 = generateMapping[SourceClass, AnotherClass] 77 | } 78 | 79 | object Example extends MyMappings { 80 | val target1 = automap(source).to[TargetClass] 81 | val target2 = automap(source).to[AnotherClass] 82 | } 83 | ``` 84 | 85 | This example triggers the macro to generate the `Mapping` into the `MyMappings` trait, while the previous example used an implicit conversion to automatically generate the implicit mapping on the fly. 86 | 87 | There's no real difference, obviously the first one is less verbose, but we will take a look at how to generate more complex mappings that require the mappings to be generated separately. 88 | 89 | If some of the fields cannot be mapped automatically a compilation error will occur notifying the missing fields. In this case we can fill out the blanks by using dynamic mappings. 90 | 91 | ### Dynamic mappings 92 | 93 | It is pretty common to want to rename a field, or to have a calculated field into the target class that depend on the source class or other variables. 94 | 95 | A dynamic mapping can be used to be able to partially map case classes with custom logic. 96 | 97 | Take a look at the following example: 98 | 99 | ```scala 100 | case class SourceClass(label: String, field: String, list: List[Int]) 101 | case class TargetClass(label: String, renamedField: String, total: Int) 102 | ``` 103 | 104 | The label field can be automatically mapped, but not the other 2, here is how you can specify a dynamic mapping for those fields: 105 | 106 | ```scala 107 | import io.bfil.automapper._ 108 | 109 | val source = SourceClass("label", "field", List(1, 2, 3)) 110 | 111 | val values = source.list 112 | def sum(values: List[Int]) = values.sum 113 | 114 | val target = automap(source).dynamicallyTo[TargetClass]( 115 | renamedField = source.field, total = sum(values) 116 | ) 117 | ``` 118 | 119 | The example is unnecessarily complex just to demonstrate that it's possible to write any type of custom logic for the dynamic mapping (or at least I haven't found other issues so far). 120 | 121 | Note that we didn't have to provide a value for the `label` field, since it could be automatically mapped. 122 | 123 | ### Implicit conversions & polymorphic types 124 | 125 | Implicit conversions can be used to fill in gaps between fields where necessary, helping to reduce boilerplate. 126 | 127 | Polymorphic types are one example where implicit conversions can help. Polymorphic types are not automatically mapped, but an implicit conversion between two traits can be provided in scope. 128 | 129 | Using the folling example: 130 | 131 | ```scala 132 | trait SourceTrait 133 | case class SourceClassA(label: String, value: Int) extends SourceTrait 134 | case class SourceClassB(width: Int) extends SourceTrait 135 | 136 | trait TargetTrait 137 | case class TargetClassA(label: String, value: Int) extends TargetTrait 138 | case class TargetClassB(width: Int) extends TargetTrait 139 | 140 | case class SourceClass(field: SourceTrait) 141 | case class TargetClass(field: TargetTrait) 142 | ``` 143 | 144 | You can define an implicit conversion from `SourceTrait` to `TargetTrait`: 145 | 146 | ```scala 147 | import io.bfil.automapper._ 148 | 149 | implicit def mapTrait(source: SourceTrait): TargetTrait = source match { 150 | case a: SourceClassA => automap(a).to[TargetClassA] 151 | case b: SourceClassB => automap(b).to[TargetClassB] 152 | } 153 | ``` 154 | 155 | With the implicit conversion between `SourceClass` to `TargetClass` in scope automapping two classes will work as expected: 156 | 157 | ```scala 158 | import io.bfil.automapper._ 159 | 160 | val source = SourceClass(SourceClassA("label", 10)) 161 | val target = automap(source).to[TargetClass] 162 | ``` 163 | 164 | The same applies for any other implicit conversion available in scope. 165 | 166 | ### Mapping rules 167 | 168 | To fully understand how the mapping takes place here are some basic rules that are applied by the macro when generating the mapping: 169 | 170 | 1. The dynamic mapping takes precedence over everything else 171 | 2. `Option` fields will be filled in with a value of `None` if the source class does not contain the field 172 | 3. `Iterable` and `Map` fields will be filled in with an empty `Iterable` / `Map` if the source class does not contain the field 173 | 4. If the target class has a field with a default value it will be used if the source class does not contain the field 174 | 5. Due to how the mapping is generated default values for `Option` / `Iterable` / `Map` fields will not be considered and a `None` or empty value will be used into the target class instead 175 | 176 | ### Generated code 177 | 178 | To give some insight on how the macro generated code looks like, here are some examples taken [from the tests](https://github.com/bfil/scala-automapper/blob/master/automapper/src/test/scala/com/bfil/automapper/AutoMappingSpec.scala). 179 | 180 | Here is our example source class: 181 | 182 | ```scala 183 | case class SourceClass( 184 | field: String, 185 | data: SourceData, 186 | list: List[Int], 187 | typedList: List[SourceData], 188 | optional: Option[String], 189 | typedOptional: Option[SourceData], 190 | map: Map[String, Int], 191 | typedMap: Map[String, SourceData], 192 | level1: SourceLevel1) 193 | 194 | case class SourceData(label: String, value: Int) 195 | case class SourceLevel1(level2: Option[SourceLevel2]) 196 | case class SourceLevel2(treasure: String) 197 | ``` 198 | 199 | #### Without dynamic mapping 200 | 201 | The code without dynamic mapping looks pretty much as it would look like if the mapping was created manually. 202 | 203 | This is how the target class looks like, basically it's just a mirror of the source class: 204 | 205 | ```scala 206 | case class TargetClass( 207 | field: String, 208 | data: TargetData, 209 | list: List[Int], 210 | typedList: List[TargetData], 211 | optional: Option[String], 212 | typedOptional: Option[TargetData], 213 | map: Map[String, Int], 214 | typedMap: Map[String, TargetData], 215 | level1: TargetLevel1) 216 | 217 | case class TargetData(label: String, value: Int) 218 | case class TargetLevel1(level2: Option[TargetLevel2]) 219 | case class TargetLevel2(treasure: String) 220 | ``` 221 | 222 | And here is the mapping generated by the macro: 223 | 224 | ```scala 225 | { 226 | import io.bfil.automapper.Mapping; 227 | { 228 | final class $anon extends Mapping[SourceClass, TargetClass] { 229 | def map(a: SourceClass): TargetClass = TargetClass( 230 | field = a.field, 231 | data = TargetData(label = a.data.label, value = a.data.value), 232 | list = a.list, 233 | typedList = a.typedList.map(((a) => TargetData(label = a.label, value = a.value))), 234 | optional = a.optional, 235 | typedOptional = a.typedOptional.map(((a) => TargetData(label = a.label, value = a.value))), 236 | map = a.map, 237 | typedMap = a.typedMap.mapValues(((a) => TargetData(label = a.label, value = a.value))), 238 | level1 = TargetLevel1(level2 = a.level1.level2.map(((a) => TargetLevel2(treasure = a.treasure))))) 239 | }; 240 | new $anon() 241 | } 242 | } 243 | ``` 244 | 245 | #### With dynamic mapping 246 | 247 | The code with dynamic mapping has the only overhead of having to use an instance of `Dynamic`, so it looks a little bit different. 248 | 249 | This is how the target class looks like: 250 | 251 | ```scala 252 | case class TargetWithDynamicMapping(renamedField: String, data: TargetData, total: Int) 253 | ``` 254 | 255 | Here is how the dynamic mapping looks like: 256 | 257 | ```scala 258 | val values = source.list 259 | def sum(values: List[Int]) = values.sum 260 | 261 | automap(source).dynamicallyTo[TargetWithDynamicMapping]( 262 | renamedField = source.field, total = sum(values) 263 | ) 264 | ``` 265 | 266 | And finally, here is the mapping generated by the macro: 267 | 268 | ```scala 269 | { 270 | import io.bfil.automapper.Mapping; 271 | { 272 | final class $anon extends Mapping[SourceClass, TargetWithDynamicMapping] { 273 | def map(a: SourceClass): TargetWithDynamicMapping = { 274 | TargetWithDynamicMapping( 275 | renamedField = source.field, 276 | data = TargetData(label = a.data.label, value = a.data.value), 277 | total = sum(values) 278 | ) 279 | } 280 | }; 281 | new $anon() 282 | } 283 | } 284 | ``` 285 | 286 | Pretty cool. Huh? 287 | 288 | License 289 | ------- 290 | 291 | This software is licensed under the Apache 2 license, quoted below. 292 | 293 | Copyright © 2015-2017 Bruno Filippone 294 | 295 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 296 | use this file except in compliance with the License. You may obtain a copy of 297 | the License at 298 | 299 | [http://www.apache.org/licenses/LICENSE-2.0] 300 | 301 | Unless required by applicable law or agreed to in writing, software 302 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 303 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 304 | License for the specific language governing permissions and limitations under 305 | the License. 306 | -------------------------------------------------------------------------------- /automapper-macros/src/main/scala/io/bfil/automapper/Mapping.scala: -------------------------------------------------------------------------------- 1 | package io.bfil.automapper 2 | 3 | import scala.language.experimental.macros 4 | import scala.reflect.macros.blackbox.Context 5 | 6 | trait Mapping[A, B] { 7 | def map(a: A): B 8 | } 9 | 10 | object Mapping { 11 | 12 | implicit def materializeMapping[A, B]: Mapping[A, B] = macro materializeMappingImpl[A, B] 13 | 14 | def materializeMappingImpl[A: c.WeakTypeTag, B: c.WeakTypeTag](c: Context): c.Expr[Mapping[A, B]] = generateMapping[A, B](c)(Seq.empty) 15 | 16 | def materializeDynamicMappingImpl[A: c.WeakTypeTag, B: c.WeakTypeTag](c: Context)(name: c.Expr[String])(args: c.Expr[(String, Any)]*): c.Expr[B] = { 17 | import c.universe._ 18 | 19 | val c.Expr(Literal(Constant(methodName))) = name 20 | 21 | val dynamicParams = args.flatMap { 22 | _.tree.collect { 23 | case arg @ Apply(TypeApply(Select(Select(Ident(scala), tuple2), TermName("apply")), List(TypeTree(), TypeTree())), List(Literal(Constant(key: String)), impl)) => arg 24 | } 25 | } 26 | 27 | val mapping = methodName match { 28 | case "dynamicallyTo" => 29 | generateMapping[A, B](c)(dynamicParams) 30 | case methodName => 31 | c.error(name.tree.pos, s"not found value $methodName in io.bfil.automapper.PartialMapping") 32 | generateMapping[A, B](c)(dynamicParams) 33 | } 34 | 35 | val source = c.macroApplication.collect { 36 | case arg @ Apply(TypeApply(Select(Select(_, _), TermName("automap")), List(TypeTree())), List(source)) => source 37 | }.headOption 38 | 39 | if(source.isEmpty) c.error(c.enclosingPosition, "Unable to resolve source reference to be used for auto mapping") 40 | 41 | def generateCode() = q"""$mapping.map(${source.get}): ${weakTypeOf[B]}""" 42 | 43 | c.Expr[B](generateCode()) 44 | } 45 | 46 | private def generateMapping[A: c.WeakTypeTag, B: c.WeakTypeTag](c: Context)(dynamicParams: Seq[c.universe.Apply]): c.Expr[Mapping[A, B]] = { 47 | import c.universe._ 48 | 49 | import scala.util.control.ControlThrowable 50 | class AutomapperException(val pos: Position, val msg: String) extends ControlThrowable(msg) 51 | 52 | val sourceType = weakTypeOf[A] 53 | val targetType = weakTypeOf[B] 54 | 55 | if(targetType =:= typeOf[Nothing]) { 56 | c.error(c.enclosingPosition, "Unable to infer target type for auto mapping") 57 | } 58 | 59 | val targetCompanion = targetType.typeSymbol.companion 60 | 61 | def getFirstTypeParam(tpe: Type) = { val TypeRef(_, _, tps) = tpe; tps.head } 62 | def getSecondTypeParam(tpe: Type) = { val TypeRef(_, _, tps) = tpe; tps.tail.head } 63 | 64 | def isOptionSymbol(typeSymbol: Symbol) = typeSymbol == typeOf[Option[_]].typeSymbol 65 | def isCaseClassSymbol(typeSymbol: Symbol) = typeSymbol.isClass && typeSymbol.asClass.isCaseClass 66 | def isIterableType(tpe: Type): Boolean = tpe.baseClasses.contains(typeOf[Iterable[_]].typeSymbol) && !isMapType(tpe) 67 | def isMapType(tpe: Type): Boolean = tpe.baseClasses.contains(typeOf[Map[_, _]].typeSymbol) 68 | 69 | def getFields(tpe: Type): List[FieldInfo] = 70 | tpe.decls.collectFirst { 71 | case m: MethodSymbol if m.isPrimaryConstructor => m 72 | }.map(_.paramLists.head.map(FieldInfo)).getOrElse(List.empty) 73 | 74 | case class FieldInfo(field: Symbol) { 75 | lazy val term = field.asTerm 76 | lazy val termName = term.name 77 | lazy val key = termName.decodedName.toString 78 | lazy val tpe = term.typeSignature 79 | lazy val typeSymbol = tpe.typeSymbol 80 | lazy val isCaseClass = isCaseClassSymbol(typeSymbol) 81 | lazy val isOptional = isOptionSymbol(typeSymbol) 82 | lazy val isOptionalCaseClass = isOptional && isCaseClassSymbol(getFirstTypeParam(tpe).typeSymbol) 83 | lazy val isIterable = isIterableType(tpe) 84 | lazy val isIterableCaseClass = isIterable && isCaseClassSymbol(getFirstTypeParam(tpe).typeSymbol) 85 | lazy val isMap = isMapType(tpe) 86 | lazy val companion = typeSymbol.companion 87 | lazy val firstTypeParamCompanion = 88 | if (isOptional || isIterable) getFirstTypeParam(tpe).typeSymbol.companion 89 | else throw new NoSuchElementException(s"$key is of type $tpe and does not have a type parameter") 90 | lazy val secondTypeParamCompanion = 91 | if (isMap) getSecondTypeParam(tpe).typeSymbol.companion 92 | else throw new NoSuchElementException(s"$key is of type $tpe and does not have a second type parameter") 93 | } 94 | 95 | def extractParams(sourceType: Type, targetType: Type, parentFields: List[FieldInfo], isRoot: Boolean = true): List[Tree] = { 96 | 97 | val sourceFields = getFields(sourceType) 98 | val targetFields = getFields(targetType) 99 | 100 | targetFields.map { targetField => 101 | 102 | val sourceFieldOption = sourceFields.find(_.key == targetField.key) 103 | 104 | val targetFieldLiteral = Literal(Constant(targetField.key)) 105 | val dynamicField = dynamicParams.find { term => 106 | term.children(1).equalsStructure(targetFieldLiteral) 107 | } 108 | 109 | if (dynamicField.isDefined && isRoot) { 110 | NamedArg(Ident(targetField.termName), dynamicField.get.children(2)) 111 | } else if (sourceFieldOption.isDefined) { 112 | 113 | val sourceField = sourceFieldOption.get 114 | 115 | val fieldSelector = (parentFields ++ List(sourceField)).foldLeft(Ident(TermName("a")): Tree) { 116 | case (tree, field) => Select(tree, field.termName) 117 | } 118 | 119 | val sourceAndTargetHaveDifferentTypes = sourceField.tpe != targetField.tpe || 120 | (sourceField.isMap && getSecondTypeParam(sourceField.tpe) != getSecondTypeParam(targetField.tpe)) 121 | 122 | val value = { 123 | if (sourceAndTargetHaveDifferentTypes) { 124 | 125 | if (targetField.isOptionalCaseClass || targetField.isIterableCaseClass) { 126 | val params = extractParams(getFirstTypeParam(sourceField.tpe), getFirstTypeParam(targetField.tpe), List.empty, false) 127 | val value = q"${targetField.firstTypeParamCompanion}(..$params)" 128 | 129 | val lambda = Apply(Select(fieldSelector, TermName("map")), 130 | List(Function(List(ValDef(Modifiers(Flag.PARAM), TermName("a"), TypeTree(), EmptyTree)), value))) 131 | 132 | q"$lambda" 133 | } else if (targetField.isMap) { 134 | val params = extractParams(getSecondTypeParam(sourceField.tpe), getSecondTypeParam(targetField.tpe), List.empty, false) 135 | val value = q"${targetField.secondTypeParamCompanion}(..$params)" 136 | 137 | val lambda = Apply(Select(fieldSelector, TermName("mapValues")), 138 | List(Function(List(ValDef(Modifiers(Flag.PARAM), TermName("a"), TypeTree(), EmptyTree)), value))) 139 | 140 | q"$lambda.toMap" 141 | } else if (targetField.isCaseClass) { 142 | val params = extractParams(sourceField.tpe, targetField.tpe, parentFields :+ sourceField, false) 143 | q"${targetField.companion}(..$params)" 144 | } else fieldSelector 145 | 146 | } else fieldSelector 147 | } 148 | 149 | q"${targetField.termName} = $value" 150 | } else { 151 | def namedAssign(value: Tree) = NamedArg(Ident(targetField.termName), value) 152 | 153 | if (targetField.isOptional) namedAssign(q"None") 154 | else if (targetField.isIterable) namedAssign(q"${targetField.companion}.empty") 155 | else if (targetField.isMap) namedAssign(q"Map.empty") 156 | else EmptyTree 157 | } 158 | }.filter(_.nonEmpty) 159 | } 160 | 161 | val params = extractParams(sourceType, targetType, List.empty) 162 | 163 | def generateCode() = 164 | q""" 165 | new io.bfil.automapper.Mapping[$sourceType, $targetType] { 166 | def map(a: $sourceType): $targetType = { 167 | $targetCompanion(..$params) 168 | } 169 | } 170 | """ 171 | 172 | c.Expr[Mapping[A, B]](generateCode()) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /automapper/src/main/scala/io/bfil/automapper/package.scala: -------------------------------------------------------------------------------- 1 | package io.bfil 2 | 3 | import scala.language.dynamics 4 | import scala.language.experimental.macros 5 | 6 | package object automapper { 7 | def automap[A](a: A): PartialMapping[A] = new PartialMapping(a) 8 | class PartialMapping[A](a: A) extends Dynamic { 9 | def to[B](implicit mapping: Mapping[A, B]): B = mapping.map(a) 10 | def applyDynamicNamed[B](name: String)(args: (String, Any)*): B = 11 | macro Mapping.materializeDynamicMappingImpl[A, B] 12 | } 13 | def generateMapping[A, B]: Mapping[A, B] = macro Mapping.materializeMappingImpl[A, B] 14 | } 15 | -------------------------------------------------------------------------------- /automapper/src/test/scala/io/bfil/automapper/AutomapperSpec.scala: -------------------------------------------------------------------------------- 1 | package io.bfil.automapper 2 | 3 | import org.scalatest._ 4 | 5 | class AutomapperSpec extends WordSpec with Matchers with TestData { 6 | 7 | "automap" should { 8 | 9 | "map a case class to another case class as expected" in { 10 | 11 | automap(source).to[TargetClass] === target 12 | 13 | } 14 | 15 | "map a case class with missing optionals to another case class as expected" in { 16 | 17 | val sourceWithMissingOptionals = SourceClass("field", sourceData, sourceValues, sourceDatas, None, None, sourceMap, sourceMapWithData, sourceLevel1) 18 | val targetWithMissingOptionals = TargetClass("field", targetData, targetValues, targetDatas, None, None, targetMap, targetMapWithData, targetLevel1) 19 | 20 | automap(sourceWithMissingOptionals).to[TargetClass] === targetWithMissingOptionals 21 | 22 | } 23 | 24 | "map a case class to another case class with a subset of fields" in { 25 | 26 | automap(source).to[TargetSubset] === TargetSubset(targetData) 27 | 28 | } 29 | 30 | "map a case class to another case class by setting None for fields not present in the first class" in { 31 | 32 | automap(source).to[TargetWithOptionalUnexpectedField] === TargetWithOptionalUnexpectedField(targetData, None) 33 | 34 | } 35 | 36 | "map a case class to another case class by setting an empty iterable for fields not present in the first class" in { 37 | 38 | automap(source).to[TargetWithUnexpectedList] === TargetWithUnexpectedList(targetData, List.empty) 39 | 40 | } 41 | 42 | "map a case class to another case class by setting an empty map for fields not present in the first class" in { 43 | 44 | automap(source).to[TargetWithUnexpectedMap] === TargetWithUnexpectedMap(targetData, Map.empty) 45 | 46 | } 47 | 48 | "map a case class to another case class by setting the default value for fields not present in the first class" in { 49 | 50 | automap(source).to[TargetWithDefaultValue] === TargetWithDefaultValue(targetData) 51 | 52 | } 53 | 54 | "map a case class to another case class when using a qualified type" in { 55 | 56 | automap(SomeObject.Source("value", SomeObject.Data(1))).to[AnotherObject.Target] === AnotherObject.Target("value", AnotherObject.Data(1)) 57 | 58 | } 59 | 60 | "not compile if mapping cannot be generated" in { 61 | 62 | "automap(source).to[TargetWithUnexpectedField]" shouldNot compile 63 | 64 | } 65 | 66 | } 67 | 68 | "automap dynamically" should { 69 | 70 | val values = source.list 71 | def sum(values: List[Int]) = values.sum 72 | 73 | "map a case class to another case class allowing dynamic fields mapping" in { 74 | 75 | automap(source).dynamicallyTo[TargetWithDynamicMapping]( 76 | renamedField = source.field, 77 | total = sum(values) 78 | ) === TargetWithDynamicMapping("field", targetData, 6) 79 | 80 | } 81 | 82 | "not compile if missing mappings have not been provided in the dynamic mapping" in { 83 | 84 | """ 85 | automap(source).dynamicallyTo[TargetWithDynamicMapping]( 86 | renamedField = source.field 87 | ) 88 | """ shouldNot compile 89 | 90 | } 91 | 92 | "not compile if typechecking fails when assigning a field dynamically" in { 93 | 94 | """ 95 | automap(source).dynamicallyTo[TargetWithDynamicMapping]( 96 | renamedField = 10, 97 | total = "value" 98 | ) 99 | """ shouldNot compile 100 | 101 | } 102 | 103 | } 104 | 105 | "automap using generated implicit mappings" should { 106 | 107 | "map a case class to another case class as expected using the manually generated implicit mappings" in { 108 | 109 | implicit val mapping = generateMapping[SourceClass, TargetClass] 110 | 111 | automap(source).to[TargetClass] === target 112 | 113 | } 114 | 115 | "map a case class to another case class as expected using the manually generated implicit mappings and be able to disambiguate between multiple implicit mappings" in { 116 | 117 | implicit val mapping = generateMapping[SourceClass, TargetClass] 118 | implicit val mappingForSubset = generateMapping[SourceClass, TargetSubset] 119 | 120 | automap(source).to[TargetClass] === target 121 | 122 | } 123 | 124 | } 125 | 126 | "automap polymorphic types" should { 127 | 128 | def mapPolymorphicTrait(source: SourcePolymorphicTrait): TargetPolymorphicTrait = source match { 129 | 130 | case a: SourcePolymorphicClassA => automap(a).to[TargetPolymorphicClassA] 131 | case b: SourcePolymorphicClassB => automap(b).to[TargetPolymorphicClassB] 132 | 133 | } 134 | 135 | "map a polymorphic type field" in { 136 | 137 | implicit val conversion = mapPolymorphicTrait _ 138 | 139 | automap(sourcePolymorphicA).to[TargetPolymorphicClass] === targetPolymorphicA 140 | automap(sourcePolymorphicB).to[TargetPolymorphicClass] === targetPolymorphicB 141 | 142 | } 143 | 144 | "throw an exception for an unmapped polymorphic type" in { 145 | 146 | assertThrows[MatchError] { 147 | 148 | implicit val conversion = mapPolymorphicTrait _ 149 | 150 | automap(sourcePolymorphicC).to[TargetPolymorphicClass] 151 | 152 | } 153 | 154 | } 155 | 156 | "not compile without an implicit conversion in scope" in { 157 | 158 | "automap(sourcePolymorphicA).to[TargetPolymorphicClass]" shouldNot compile 159 | 160 | } 161 | 162 | } 163 | 164 | } 165 | 166 | case class SourceClass( 167 | field: String, 168 | data: SourceData, 169 | list: List[Int], 170 | typedList: List[SourceData], 171 | optional: Option[String], 172 | typedOptional: Option[SourceData], 173 | map: Map[String, Int], 174 | typedMap: Map[String, SourceData], 175 | level1: SourceLevel1) 176 | 177 | case class SourceData(label: String, value: Int) 178 | case class SourceLevel1(level2: Option[SourceLevel2]) 179 | case class SourceLevel2(treasure: String) 180 | 181 | trait SourcePolymorphicTrait 182 | case class SourcePolymorphicClassA(label: String, value: Int) extends SourcePolymorphicTrait 183 | case class SourcePolymorphicClassB(width: Int) extends SourcePolymorphicTrait 184 | case class SourcePolymorphicClassC(title: String) extends SourcePolymorphicTrait 185 | case class SourcePolymorphicClass(field: SourcePolymorphicTrait) 186 | 187 | case class TargetClass( 188 | field: String, 189 | data: TargetData, 190 | list: List[Int], 191 | typedList: List[TargetData], 192 | optional: Option[String], 193 | typedOptional: Option[TargetData], 194 | map: Map[String, Int], 195 | typedMap: Map[String, TargetData], 196 | level1: TargetLevel1) 197 | 198 | case class TargetData(label: String, value: Int) 199 | case class TargetLevel1(level2: Option[TargetLevel2]) 200 | case class TargetLevel2(treasure: String) 201 | 202 | case class TargetSubset(data: TargetData) 203 | case class TargetWithUnexpectedField(data: TargetData, unexpectedField: Exception) 204 | case class TargetWithOptionalUnexpectedField(data: TargetData, unexpectedField: Option[Exception]) 205 | case class TargetWithUnexpectedList(data: TargetData, unexpectedList: List[Int]) 206 | case class TargetWithUnexpectedMap(data: TargetData, unexpectedMap: Map[String, Int]) 207 | case class TargetWithDefaultValue(data: TargetData, default: String = "default") 208 | case class TargetWithDynamicMapping(renamedField: String, data: TargetData, total: Int) 209 | 210 | trait TargetPolymorphicTrait 211 | case class TargetPolymorphicClassA(label: String, value: Int) extends TargetPolymorphicTrait 212 | case class TargetPolymorphicClassB(width: Int) extends TargetPolymorphicTrait 213 | case class TargetPolymorphicClass(field: TargetPolymorphicTrait) 214 | 215 | trait TestData { 216 | 217 | val sourceData = SourceData("label", 10) 218 | val sourceLevel2 = SourceLevel2("treasure") 219 | val sourceLevel1 = SourceLevel1(Some(sourceLevel2)) 220 | 221 | val sourceValues = List(1, 2, 3) 222 | val sourceDatas = List(SourceData("label1", 1), SourceData("label1", 2), SourceData("label1", 3)) 223 | val sourceMap = Map("one" -> 1, "two" -> 2) 224 | val sourceMapWithData = Map("one" -> SourceData("label1", 1), "two" -> SourceData("label2", 2)) 225 | 226 | val source = 227 | SourceClass( 228 | "field", sourceData, 229 | sourceValues, sourceDatas, 230 | Some("optional"), Some(sourceData), 231 | sourceMap, sourceMapWithData, 232 | sourceLevel1) 233 | 234 | val sourcePolymorphicA = SourcePolymorphicClass(SourcePolymorphicClassA("label", 10)) 235 | val sourcePolymorphicB = SourcePolymorphicClass(SourcePolymorphicClassB(11)) 236 | val sourcePolymorphicC = SourcePolymorphicClass(SourcePolymorphicClassC("title")) 237 | 238 | val targetData = TargetData("label", 10) 239 | val targetLevel2 = TargetLevel2("treasure") 240 | val targetLevel1 = TargetLevel1(Some(targetLevel2)) 241 | 242 | val targetValues = List(1, 2, 3) 243 | val targetDatas = List(TargetData("label1", 1), TargetData("label1", 2), TargetData("label1", 3)) 244 | val targetMap = Map("one" -> 1, "two" -> 2) 245 | val targetMapWithData = Map("one" -> TargetData("label1", 1), "two" -> TargetData("label2", 2)) 246 | 247 | val target = 248 | TargetClass( 249 | "field", targetData, 250 | targetValues, targetDatas, 251 | Some("optional"), Some(targetData), 252 | targetMap, targetMapWithData, 253 | targetLevel1) 254 | 255 | val targetPolymorphicA = TargetPolymorphicClass(TargetPolymorphicClassA("label", 10)) 256 | val targetPolymorphicB = TargetPolymorphicClass(TargetPolymorphicClassB(11)) 257 | 258 | } 259 | 260 | object SomeObject { 261 | case class Source(value: String, data: Data) 262 | case class Data(value: Int) 263 | } 264 | 265 | object AnotherObject { 266 | case class Target(value: String, data: Data) 267 | case class Data(value: Int) 268 | } 269 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | lazy val root = Project("root", file(".")) 2 | .settings(settings, publishArtifact := false) 3 | .aggregate(automapperMacros, automapper) 4 | 5 | lazy val automapperMacros = Project("automapper-macros", file("automapper-macros")) 6 | .settings(settings, libraryDependencies ++= Seq( 7 | "org.scala-lang" % "scala-reflect" % scalaVersion.value 8 | )) 9 | 10 | lazy val automapper = Project("automapper", file("automapper")) 11 | .settings(settings, libraryDependencies ++= Seq( 12 | "org.scalatest" %% "scalatest" % "3.0.8" % "test" 13 | )) 14 | .dependsOn(automapperMacros) 15 | 16 | lazy val settings = Seq( 17 | scalaVersion := "2.13.0", 18 | crossScalaVersions := Seq("2.13.0"), 19 | organization := "io.bfil", 20 | organizationName := "Bruno Filippone", 21 | organizationHomepage := Some(url("http://bfil.io")), 22 | homepage := Some(url("https://github.com/bfil/scala-automapper")), 23 | licenses := Seq(("Apache-2.0", url("http://www.apache.org/licenses/LICENSE-2.0"))), 24 | developers := List( 25 | Developer("bfil", "Bruno Filippone", "bruno@bfil.io", url("http://bfil.io")) 26 | ), 27 | startYear := Some(2015), 28 | publishTo := Some("Bintray" at s"https://api.bintray.com/maven/bfil/maven/${name.value}"), 29 | credentials += Credentials(Path.userHome / ".ivy2" / ".bintray-credentials"), 30 | scmInfo := Some(ScmInfo( 31 | url(s"https://github.com/bfil/scala-automapper"), 32 | s"git@github.com:bfil/scala-automapper.git" 33 | )) 34 | ) 35 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.8 2 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "0.7.0" 2 | --------------------------------------------------------------------------------