├── .gitignore ├── LICENSE ├── README.md ├── assembly.sbt ├── build.sbt ├── project ├── build.properties └── plugins.sbt └── src ├── main ├── java │ └── co │ │ └── s4n │ │ └── uuid │ │ └── UUIDGen.java └── scala │ ├── co │ └── s4n │ │ ├── comision │ │ ├── comisionaggregate.scala │ │ ├── domain │ │ │ ├── comision.scala │ │ │ └── comisionrepository.scala │ │ └── infrastructure │ │ │ └── daos.scala │ │ ├── factura │ │ ├── facturaServiceFree.scala │ │ └── facturaServices.scala │ │ └── fp │ │ ├── CuentasContablesMonoid.scala │ │ ├── monadTransformers.scala │ │ └── monoid.scala │ └── ddd │ ├── definitions.scala │ └── transformers.scala └── test └── scala └── co └── s4n ├── comision ├── ComisionServicesTest.scala ├── domain │ ├── ComisionRepositoryTest.scala │ └── ComisionTest.scala └── infrastructure │ └── TransformableTest.scala ├── factura ├── FacturaServicesTest.scala └── TicketsServiceTest.scala ├── fp ├── CuentasContablesServicesTest.scala ├── MonoidTest.scala ├── OptionTDemoTest.scala └── ReaderMonadDITest.scala └── uuid └── UUIDGenTest.scala /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # eclipse 5 | .classpath 6 | .project 7 | .settings/ 8 | .cache 9 | 10 | # idea 11 | .idea 12 | *.iml 13 | bin/ 14 | 15 | # sbt specific 16 | .cache/ 17 | .history/ 18 | .lib/ 19 | dist/* 20 | target/ 21 | lib_managed/ 22 | src_managed/ 23 | project/boot/ 24 | project/plugins/ 25 | 26 | # Scala-IDE specific 27 | .scala_dependencies 28 | .worksheet 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Ejemplo de un dominio condimentado con programación funcional 2 | 3 | Este es un ejemplo simple de cómo enriquecer los diseños tácticos de DDDesign con elementos funcionales. 4 | 5 | Se presenta la aplicación de: 6 | 7 | + ADT 8 | + Phantom typing 9 | + Type classes 10 | + Lenses 11 | + Kleisli 12 | + Monoid 13 | + Monad transformers 14 | 15 | Por terminar: 16 | 17 | + Free monad 18 | + Dependent types 19 | + Applicative functors 20 | -------------------------------------------------------------------------------- /assembly.sbt: -------------------------------------------------------------------------------- 1 | outputPath in assembly := file( "dist/scala-base-project.jar" ) 2 | 3 | test in assembly := {} 4 | 5 | mainClass in assembly := Some("co.s4n.Main") 6 | 7 | assemblyMergeStrategy in assembly := { 8 | case x => 9 | val oldStrategy = (assemblyMergeStrategy in assembly).value 10 | oldStrategy(x) 11 | } 12 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import spray.revolver.RevolverPlugin._ 2 | 3 | seq(Revolver.settings: _*) 4 | 5 | scalariformSettings 6 | 7 | name := "functional-domain" 8 | 9 | organization := "co.s4n" 10 | 11 | version := "0.0.1" 12 | 13 | scalaVersion := "2.11.7" 14 | 15 | resolvers ++= Seq( 16 | "releases" at "http://oss.sonatype.org/content/repositories/releases" 17 | ) 18 | 19 | libraryDependencies ++= Seq( 20 | "com.chuusai" %% "shapeless" % "2.2.5" withSources() withJavadoc(), 21 | "org.scalaz" %% "scalaz-core" % "7.1.4" withSources() withJavadoc(), 22 | "com.softwaremill.macwire" %% "macros" % "1.0.1" withSources() withJavadoc(), 23 | "com.softwaremill.macwire" %% "runtime" % "1.0.1" withSources() withJavadoc(), 24 | "org.joda" % "joda-money" % "0.10.0" withSources() withJavadoc(), 25 | "com.github.nscala-time" %% "nscala-time" % "2.4.0" withSources() withJavadoc(), 26 | "com.typesafe.scala-logging" %% "scala-logging" % "3.1.0" withSources() withJavadoc(), 27 | "org.scalatest" % "scalatest_2.11" % "2.2.4" % "test", 28 | "org.mockito" % "mockito-core" % "1.10.19" % "test" withSources() withJavadoc(), 29 | "org.typelevel" %% "scalaz-contrib-210" % "0.2", 30 | "org.typelevel" %% "scalaz-contrib-validation" % "0.2", 31 | "org.typelevel" %% "scalaz-contrib-undo" % "0.2", 32 | // currently unavailable because there's no 2.11 build of Lift yet 33 | // "org.typelevel" %% "scalaz-lift" % "0.2", 34 | "org.typelevel" %% "scalaz-nscala-time" % "0.2", 35 | "org.typelevel" %% "scalaz-spire" % "0.2" 36 | ) 37 | 38 | scalacOptions ++= Seq( 39 | "-deprecation", 40 | "-encoding", "UTF-8", 41 | "-feature", 42 | "-language:existentials", 43 | "-language:higherKinds", 44 | "-language:implicitConversions", 45 | "-unchecked", 46 | "-Xfatal-warnings", 47 | "-Xlint", 48 | "-Yno-adapted-args", 49 | "-Ywarn-dead-code", 50 | "-Ywarn-numeric-widen", 51 | "-Ywarn-value-discard", 52 | "-Xfuture" 53 | ) 54 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.8 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.3.0") 2 | 3 | addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.5.0") 4 | 5 | addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0") 6 | 7 | addSbtPlugin("io.spray" % "sbt-revolver" % "0.7.2") 8 | 9 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.0") 10 | 11 | addSbtPlugin("org.ensime" % "ensime-sbt" % "0.2.1") -------------------------------------------------------------------------------- /src/main/java/co/s4n/uuid/UUIDGen.java: -------------------------------------------------------------------------------- 1 | package co.s4n.uuid; 2 | 3 | import java.net.InetAddress; 4 | import java.nio.ByteBuffer; 5 | import java.security.MessageDigest; 6 | import java.security.NoSuchAlgorithmException; 7 | import java.util.Collection; 8 | import java.util.Random; 9 | import java.util.UUID; 10 | import java.net.*; 11 | import java.util.*; 12 | 13 | /** 14 | * The goods are here: www.ietf.org/rfc/rfc4122.txt. 15 | */ 16 | public class UUIDGen { 17 | // A grand day! millis at 00:00:00.000 15 Oct 1582. 18 | private static final long START_EPOCH = -12219292800000L; 19 | private static final long clockSeqAndNode = makeClockSeqAndNode(); 20 | 21 | /* 22 | * The min and max possible lsb for a UUID. 23 | * Note that his is not 0 and all 1's because Cassandra TimeUUIDType 24 | * compares the lsb parts as a signed byte array comparison. So the min 25 | * value is 8 times -128 and the max is 8 times +127. 26 | * 27 | * Note that we ignore the uuid variant (namely, MIN_CLOCK_SEQ_AND_NODE 28 | * have variant 2 as it should, but MAX_CLOCK_SEQ_AND_NODE have variant 0). 29 | * I don't think that has any practical consequence and is more robust in 30 | * case someone provides a UUID with a broken variant. 31 | */ 32 | private static final long MIN_CLOCK_SEQ_AND_NODE = 0x8080808080808080L; 33 | private static final long MAX_CLOCK_SEQ_AND_NODE = 0x7f7f7f7f7f7f7f7fL; 34 | 35 | // placement of this singleton is important. It needs to be instantiated *AFTER* the other statics. 36 | private static final UUIDGen instance = new UUIDGen(); 37 | 38 | private long lastNanos; 39 | 40 | private UUIDGen() { 41 | // make sure someone didn't whack the clockSeqAndNode by changing the order of instantiation. 42 | if (clockSeqAndNode == 0) throw new RuntimeException("singleton instantiation is misplaced."); 43 | } 44 | 45 | /** 46 | * Creates a type 1 UUID (time-based UUID). 47 | * 48 | * @return a UUID instance 49 | */ 50 | public static UUID getTimeUUID() { 51 | return new UUID(instance.createTimeSafe(), clockSeqAndNode); 52 | } 53 | 54 | /** 55 | * Creates a type 1 UUID (time-based UUID) with the timestamp of @param when, in milliseconds. 56 | * 57 | * @return a UUID instance 58 | */ 59 | public static UUID getTimeUUID(long when) { 60 | return new UUID(createTime(fromUnixTimestamp(when)), clockSeqAndNode); 61 | } 62 | 63 | public static UUID getTimeUUID(long when, long clockSeqAndNode) { 64 | return new UUID(createTime(fromUnixTimestamp(when)), clockSeqAndNode); 65 | } 66 | 67 | /** 68 | * creates a type 1 uuid from raw bytes. 69 | */ 70 | public static UUID getUUID(ByteBuffer raw) { 71 | return new UUID(raw.getLong(raw.position()), raw.getLong(raw.position() + 8)); 72 | } 73 | 74 | /** 75 | * decomposes a uuid into raw bytes. 76 | */ 77 | public static byte[] decompose(UUID uuid) { 78 | long most = uuid.getMostSignificantBits(); 79 | long least = uuid.getLeastSignificantBits(); 80 | byte[] b = new byte[16]; 81 | for (int i = 0; i < 8; i++) { 82 | b[i] = (byte) (most >>> ((7 - i) * 8)); 83 | b[8 + i] = (byte) (least >>> ((7 - i) * 8)); 84 | } 85 | return b; 86 | } 87 | 88 | /** 89 | * Returns a 16 byte representation of a type 1 UUID (a time-based UUID), 90 | * based on the current system time. 91 | * 92 | * @return a type 1 UUID represented as a byte[] 93 | */ 94 | public static byte[] getTimeUUIDBytes() { 95 | return createTimeUUIDBytes(instance.createTimeSafe()); 96 | } 97 | 98 | /** 99 | * Returns the smaller possible type 1 UUID having the provided timestamp. 100 | *

101 | * Warning: this method should only be used for querying as this 102 | * doesn't at all guarantee the uniqueness of the resulting UUID. 103 | */ 104 | public static UUID minTimeUUID(long timestamp) { 105 | return new UUID(createTime(fromUnixTimestamp(timestamp)), MIN_CLOCK_SEQ_AND_NODE); 106 | } 107 | 108 | /** 109 | * Returns the biggest possible type 1 UUID having the provided timestamp. 110 | *

111 | * Warning: this method should only be used for querying as this 112 | * doesn't at all guarantee the uniqueness of the resulting UUID. 113 | */ 114 | public static UUID maxTimeUUID(long timestamp) { 115 | // unix timestamp are milliseconds precision, uuid timestamp are 100's 116 | // nanoseconds precision. If we ask for the biggest uuid have unix 117 | // timestamp 1ms, then we should not extend 100's nanoseconds 118 | // precision by taking 10000, but rather 19999. 119 | long uuidTstamp = fromUnixTimestamp(timestamp + 1) - 1; 120 | return new UUID(createTime(uuidTstamp), MAX_CLOCK_SEQ_AND_NODE); 121 | } 122 | 123 | /** 124 | * @param uuid 125 | * @return milliseconds since Unix epoch 126 | */ 127 | public static long unixTimestamp(UUID uuid) { 128 | return (uuid.timestamp() / 10000) + START_EPOCH; 129 | } 130 | 131 | /** 132 | * @param uuid 133 | * @return microseconds since Unix epoch 134 | */ 135 | public static long microsTimestamp(UUID uuid) { 136 | return (uuid.timestamp() / 10) + START_EPOCH * 1000; 137 | } 138 | 139 | /** 140 | * @param timestamp milliseconds since Unix epoch 141 | * @return 142 | */ 143 | private static long fromUnixTimestamp(long timestamp) { 144 | return (timestamp - START_EPOCH) * 10000; 145 | } 146 | 147 | /** 148 | * Converts a 100-nanoseconds precision timestamp into the 16 byte representation 149 | * of a type 1 UUID (a time-based UUID). 150 | *

151 | * To specify a 100-nanoseconds precision timestamp, one should provide a milliseconds timestamp and 152 | * a number 0 <= n < 10000 such that n*100 is the number of nanoseconds within that millisecond. 153 | *

154 | *

Warning: This method is not guaranteed to return unique UUIDs; Multiple 155 | * invocations using identical timestamps will result in identical UUIDs.

156 | * 157 | * @return a type 1 UUID represented as a byte[] 158 | */ 159 | public static byte[] getTimeUUIDBytes(long timeMillis, int nanos) { 160 | if (nanos >= 10000) 161 | throw new IllegalArgumentException(); 162 | return createTimeUUIDBytes(instance.createTimeUnsafe(timeMillis, nanos)); 163 | } 164 | 165 | private static byte[] createTimeUUIDBytes(long msb) { 166 | long lsb = clockSeqAndNode; 167 | byte[] uuidBytes = new byte[16]; 168 | 169 | for (int i = 0; i < 8; i++) 170 | uuidBytes[i] = (byte) (msb >>> 8 * (7 - i)); 171 | 172 | for (int i = 8; i < 16; i++) 173 | uuidBytes[i] = (byte) (lsb >>> 8 * (7 - i)); 174 | 175 | return uuidBytes; 176 | } 177 | 178 | /** 179 | * Returns a milliseconds-since-epoch value for a type-1 UUID. 180 | * 181 | * @param uuid a type-1 (time-based) UUID 182 | * @return the number of milliseconds since the unix epoch 183 | * @throws IllegalArgumentException if the UUID is not version 1 184 | */ 185 | public static long getAdjustedTimestamp(UUID uuid) { 186 | if (uuid.version() != 1) 187 | throw new IllegalArgumentException("incompatible with uuid version: " + uuid.version()); 188 | return (uuid.timestamp() / 10000) + START_EPOCH; 189 | } 190 | 191 | private static long makeClockSeqAndNode() { 192 | long clock = new Random(System.currentTimeMillis()).nextLong(); 193 | 194 | long lsb = 0; 195 | lsb |= 0x8000000000000000L; // variant (2 bits) 196 | lsb |= (clock & 0x0000000000003FFFL) << 48; // clock sequence (14 bits) 197 | lsb |= makeNode(); // 6 bytes 198 | return lsb; 199 | } 200 | 201 | // needs to return two different values for the same when. 202 | // we can generate at most 10k UUIDs per ms. 203 | private synchronized long createTimeSafe() { 204 | long nanosSince = (System.currentTimeMillis() - START_EPOCH) * 10000; 205 | if (nanosSince > lastNanos) 206 | lastNanos = nanosSince; 207 | else 208 | nanosSince = ++lastNanos; 209 | 210 | return createTime(nanosSince); 211 | } 212 | 213 | private long createTimeUnsafe(long when, int nanos) { 214 | long nanosSince = ((when - START_EPOCH) * 10000) + nanos; 215 | return createTime(nanosSince); 216 | } 217 | 218 | private static long createTime(long nanosSince) { 219 | long msb = 0L; 220 | msb |= (0x00000000ffffffffL & nanosSince) << 32; 221 | msb |= (0x0000ffff00000000L & nanosSince) >>> 16; 222 | msb |= (0xffff000000000000L & nanosSince) >>> 48; 223 | msb |= 0x0000000000001000L; // sets the version to 1. 224 | return msb; 225 | } 226 | 227 | private static long makeNode() { 228 | /* 229 | * We don't have access to the MAC address but need to generate a node part 230 | * that identify this host as uniquely as possible. 231 | * The spec says that one option is to take as many source that identify 232 | * this node as possible and hash them together. That's what we do here by 233 | * gathering all the ip of this host. 234 | * Note that FBUtilities.getBroadcastAddress() should be enough to uniquely 235 | * identify the node *in the cluster* but it triggers DatabaseDescriptor 236 | * instanciation and the UUID generator is used in Stress for instance, 237 | * where we don't want to require the yaml. 238 | */ 239 | Collection localAddresses = getAllLocalAddresses(); 240 | if (localAddresses.isEmpty()) 241 | throw new RuntimeException("Cannot generate the node component of the UUID because cannot retrieve any IP addresses."); 242 | 243 | // ideally, we'd use the MAC address, but java doesn't expose that. 244 | byte[] hash = hash(localAddresses); 245 | long node = 0; 246 | for (int i = 0; i < Math.min(6, hash.length); i++) 247 | node |= (0x00000000000000ff & (long) hash[i]) << (5 - i) * 8; 248 | assert (0xff00000000000000L & node) == 0; 249 | 250 | // Since we don't use the mac address, the spec says that multicast 251 | // bit (least significant bit of the first octet of the node ID) must be 1. 252 | return node | 0x0000010000000000L; 253 | } 254 | 255 | private static byte[] hash(Collection data) { 256 | try { 257 | MessageDigest messageDigest = MessageDigest.getInstance("MD5"); 258 | for (InetAddress addr : data) 259 | messageDigest.update(addr.getAddress()); 260 | 261 | return messageDigest.digest(); 262 | } catch (NoSuchAlgorithmException nsae) { 263 | throw new RuntimeException("MD5 digest algorithm is not available", nsae); 264 | } 265 | } 266 | 267 | private static Collection getAllLocalAddresses() 268 | { 269 | Set localAddresses = new HashSet(); 270 | try 271 | { 272 | Enumeration nets = NetworkInterface.getNetworkInterfaces(); 273 | if (nets != null) 274 | { 275 | while (nets.hasMoreElements()) 276 | localAddresses.addAll(Collections.list(nets.nextElement().getInetAddresses())); 277 | } 278 | } 279 | catch (SocketException e) 280 | { 281 | throw new AssertionError(e); 282 | } 283 | return localAddresses; 284 | } 285 | } 286 | 287 | // for the curious, here is how I generated START_EPOCH 288 | // Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT-0")); 289 | // c.set(Calendar.YEAR, 1582); 290 | // c.set(Calendar.MONTH, Calendar.OCTOBER); 291 | // c.set(Calendar.DAY_OF_MONTH, 15); 292 | // c.set(Calendar.HOUR_OF_DAY, 0); 293 | // c.set(Calendar.MINUTE, 0); 294 | // c.set(Calendar.SECOND, 0); 295 | // c.set(Calendar.MILLISECOND, 0); 296 | // long START_EPOCH = c.getTimeInMillis(); 297 | -------------------------------------------------------------------------------- /src/main/scala/co/s4n/comision/comisionaggregate.scala: -------------------------------------------------------------------------------- 1 | package co.s4n.comision 2 | 3 | import co.s4n.comision.domain._ 4 | 5 | class ComisionServices extends ComisionRepositoryModule { 6 | import scala.util.Try 7 | 8 | def crear(c: Comision[Nueva]): Comision[Nueva] = { 9 | val id = comisionRepository.add(c) 10 | c.copy(id = Some(id)) 11 | } 12 | 13 | def liquidar(c: Comision[Nueva]): Try[Comision[Liquidada]] = { 14 | Try { 15 | c.copy(estado = Liquidada()) 16 | } 17 | } 18 | 19 | def anular(c: Comision[Liquidada]): Try[Comision[Anulada]] = { 20 | Try { 21 | c.copy(estado = Anulada()) 22 | } 23 | } 24 | 25 | def aprobar(c: Comision[Liquidada]): Try[Comision[Aprobada]] = { 26 | Try { 27 | c.copy(estado = Aprobada()) 28 | } 29 | } 30 | 31 | def facturar(c: Comision[Aprobada]): Try[Comision[Facturada]] = { 32 | Try { 33 | c.copy(estado = Facturada()) 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/scala/co/s4n/comision/domain/comision.scala: -------------------------------------------------------------------------------- 1 | package co.s4n.comision.domain 2 | 3 | import scalaz._ 4 | 5 | case class Cliente(id: String, nombre: String) 6 | 7 | sealed trait EstadoComision 8 | 9 | final case class Nueva() extends EstadoComision 10 | 11 | final case class Liquidada() extends EstadoComision 12 | 13 | final case class Anulada() extends EstadoComision 14 | 15 | final case class Aprobada() extends EstadoComision 16 | 17 | final case class Facturada() extends EstadoComision 18 | 19 | // http://stackoverflow.com/questions/4531455/whats-the-difference-between-ab-and-b-in-scala/4531696#4531696 20 | case class Comision[+Estado <: EstadoComision]( 21 | id: Option[Long], 22 | valorComision: Long, 23 | iva: Long, 24 | estado: Estado, 25 | cliente: Cliente 26 | ) 27 | 28 | object Comision { 29 | 30 | type NombreCliente = String 31 | 32 | private def cambiarCliente[Estado <: EstadoComision](): Lens[Comision[EstadoComision], Cliente] = { 33 | Lens.lensu[Comision[EstadoComision], Cliente]( 34 | set = (comision, nuevoCliente) => comision.copy(cliente = nuevoCliente), 35 | get = (comision) => comision.cliente 36 | ) 37 | } 38 | 39 | private def cambiarNombreCliente(): Lens[Cliente, NombreCliente] = { 40 | Lens.lensu[Cliente, String]( 41 | set = (cliente, nuevoNombre) => cliente.copy(nombre = nuevoNombre), 42 | get = (cliente) => cliente.nombre 43 | ) 44 | } 45 | 46 | def responsable[Estado <: EstadoComision](): LensFamily[Comision[EstadoComision], Comision[EstadoComision], String, String] = 47 | cambiarCliente andThen cambiarNombreCliente 48 | 49 | } -------------------------------------------------------------------------------- /src/main/scala/co/s4n/comision/domain/comisionrepository.scala: -------------------------------------------------------------------------------- 1 | package co.s4n.comision.domain 2 | 3 | import co.s4n.comision.infrastructure._ 4 | import ddd._ 5 | 6 | object ComisionRepository { 7 | 8 | implicit def comisionTransformable[State <: EstadoComision] = new Transformable[Comision[State], (ComisionRecord, ClienteRecord)] { 9 | def toRecord(entity: Comision[State]): (ComisionRecord, ClienteRecord) = { 10 | 11 | val comisionRecord = new ComisionRecord( 12 | id = entity.id, 13 | valorComision = entity.valorComision, 14 | iva = entity.iva, 15 | estado = entity.estado.toString, 16 | idCliente = entity.cliente.id 17 | ) 18 | 19 | val clienteRecord = new ClienteRecord( 20 | id = entity.cliente.id, 21 | nombre = entity.cliente.nombre 22 | ) 23 | 24 | (comisionRecord, clienteRecord) 25 | } 26 | } 27 | 28 | } 29 | 30 | class ComisionRepository(comisionDAO: ComisionDAO, clienteDAO: ClienteDAO) extends Repository[Comision[EstadoComision]] { 31 | import co.s4n.comision.domain.ComisionRepository._ 32 | 33 | override def add(entity: Comision[EstadoComision]): Long = { 34 | val r: (ComisionRecord, ClienteRecord) = Transformer.toRecord(entity) 35 | comisionDAO.insert(r._1) 36 | clienteDAO.insert(r._2) 37 | } 38 | 39 | override def get(id: Long): Comision[EstadoComision] = ??? 40 | 41 | override def remove(entity: Comision[EstadoComision]): Long = ??? 42 | 43 | override def list(): List[Comision[EstadoComision]] = ??? 44 | } 45 | 46 | trait ComisionRepositoryModule { 47 | import com.softwaremill.macwire._ 48 | 49 | lazy val comisionDAO = wire[ComisionDAO] 50 | lazy val clienteDAO = wire[ClienteDAO] 51 | lazy val comisionRepository = wire[ComisionRepository] 52 | } 53 | -------------------------------------------------------------------------------- /src/main/scala/co/s4n/comision/infrastructure/daos.scala: -------------------------------------------------------------------------------- 1 | package co.s4n.comision.infrastructure 2 | 3 | import ddd.DAO 4 | 5 | case class ComisionRecord(id: Option[Long], valorComision: Long, iva: Long, estado: String, idCliente: String) 6 | 7 | case class ClienteRecord(id: String, nombre: String) 8 | 9 | class ClienteDAO extends DAO[ClienteRecord] { 10 | override def insert(record: ClienteRecord): Long = 1L 11 | 12 | override def update(record: ClienteRecord): Long = 1L 13 | 14 | override def delete(record: ClienteRecord): Long = 1L 15 | 16 | override def retrieve(): List[ClienteRecord] = ??? 17 | } 18 | 19 | class ComisionDAO extends DAO[ComisionRecord] { 20 | override def insert(record: ComisionRecord): Long = 1L 21 | 22 | override def update(record: ComisionRecord): Long = 1L 23 | 24 | override def delete(record: ComisionRecord): Long = 1L 25 | 26 | override def retrieve(): List[ComisionRecord] = ??? 27 | } 28 | -------------------------------------------------------------------------------- /src/main/scala/co/s4n/factura/facturaServiceFree.scala: -------------------------------------------------------------------------------- 1 | package co.s4n.factura 2 | 3 | import scala.concurrent.Future 4 | import scalaz._ 5 | import Scalaz._ 6 | 7 | case class Ticket(id: String, event: String) 8 | 9 | sealed trait Request[A] 10 | final case class Buy(n: Int) extends Request[List[Ticket]] 11 | final case class Discounts(ts: List[Ticket]) extends Request[List[Ticket]] 12 | final case class VIP(ts: List[Ticket]) extends Request[List[Ticket]] 13 | final case class Format(ts: List[Ticket]) extends Request[String] 14 | final case class IsBonus(ts: Ticket) extends Request[Boolean] 15 | 16 | object TicketsService { 17 | type Requestable[A] = Free.FreeC[Request, A] 18 | 19 | implicit val RequestMonad: Monad[Requestable] = 20 | Free.freeMonad[({ type T[A] = Coyoneda[Request, A] })#T] 21 | 22 | def buy(n: Int): Requestable[List[Ticket]] = 23 | Free.liftFC(Buy(n)) 24 | 25 | def discounts(ts: List[Ticket]): Requestable[List[Ticket]] = 26 | Free.liftFC(Discounts(ts)) 27 | 28 | def vip(ts: List[Ticket]): Requestable[List[Ticket]] = 29 | Free.liftFC(VIP(ts)) 30 | 31 | def isBonus(t: Ticket): Requestable[Boolean] = { 32 | Free.liftFC(IsBonus(t)) 33 | } 34 | 35 | def format(ts: List[Ticket]): Requestable[String] = 36 | Free.liftFC(Format(ts)) 37 | 38 | } 39 | 40 | object OptionInterpreter extends NaturalTransformation[Request, Option] { 41 | 42 | def apply[A](request: Request[A]): Option[A] = { 43 | request match { 44 | case b: Buy => Some(List(Ticket("", ""))) 45 | case d: Discounts => Some(List(Ticket("", ""))) 46 | case v: VIP => Some(List(Ticket("", ""))) 47 | case ib: IsBonus => Some(true) 48 | case f: Format => Some("") 49 | } 50 | } 51 | } 52 | 53 | object ListInterpreter extends NaturalTransformation[Request, List] { 54 | 55 | def apply[A](request: Request[A]): List[A] = { 56 | request match { 57 | case b: Buy => List(List(Ticket("", ""))) 58 | case d: Discounts => List(List(Ticket("", ""))) 59 | case v: VIP => List(List(Ticket("", ""))) 60 | case ib: IsBonus => List(true) 61 | case f: Format => List("") 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/scala/co/s4n/factura/facturaServices.scala: -------------------------------------------------------------------------------- 1 | package co.s4n.factura 2 | 3 | import java.math.RoundingMode 4 | import org.joda.money.Money 5 | import org.joda.time.DateTime 6 | import scala.util.Try 7 | 8 | sealed trait EstadoFactura 9 | final case class Nueva() extends EstadoFactura 10 | final case class Anulada() extends EstadoFactura 11 | final case class Aprobada() extends EstadoFactura 12 | final case class Causada(valor: Money) extends EstadoFactura 13 | 14 | case class Factura[State <: EstadoFactura](fecha: DateTime, idCliente: String, valor: Money, iva: Double, state: State) 15 | 16 | trait FacturaServices { 17 | 18 | val anular: Factura[Nueva] => Try[Factura[Anulada]] = { 19 | facturaNueva => 20 | Try(facturaNueva.copy(state = new Anulada())) 21 | } 22 | 23 | val aprobar: Factura[Nueva] => Try[Factura[Aprobada]] = { 24 | facturaNueva => 25 | Try(facturaNueva.copy(state = new Aprobada())) 26 | } 27 | 28 | val causar: Factura[Aprobada] => Try[Factura[Causada]] = { 29 | factura => 30 | Try { 31 | val valor: Money = factura.valor.multipliedBy( 32 | 1.0 + factura.iva, 33 | RoundingMode.CEILING 34 | ) 35 | factura.copy(state = new Causada(valor)) 36 | } 37 | } 38 | } 39 | 40 | object FacturaServices extends FacturaServices 41 | -------------------------------------------------------------------------------- /src/main/scala/co/s4n/fp/CuentasContablesMonoid.scala: -------------------------------------------------------------------------------- 1 | package co.s4n.fp 2 | 3 | import co.s4n.factura.{ Nueva, Factura } 4 | import org.joda.money.{ CurrencyUnit, Money } 5 | import scalaz.Monoid 6 | import scalaz.syntax.monoid._ 7 | 8 | case class CuentaContable(tipo: String, valor: Money) 9 | 10 | trait CuentasContablesServices { 11 | 12 | def balanceDelTipo(tipo: String, cuentasDelDia: List[CuentaContable])(implicit m: Monoid[Money]): Money = { 13 | cuentasDelDia 14 | .filter(_.tipo == tipo) 15 | .foldLeft(mzero[Money])(_ |+| _.valor) 16 | } 17 | 18 | def maximaCuentaDelTipo(tipo: String, cuentasDelDia: List[CuentaContable])(implicit m: Monoid[Money]): Money = { 19 | cuentasDelDia 20 | .filter(_.tipo == tipo) 21 | .foldLeft(mzero[Money]) { 22 | (acc, cuenta) => 23 | if (cuenta.valor isGreaterThan acc) 24 | cuenta.valor 25 | else 26 | acc 27 | } 28 | } 29 | 30 | def balanceFacturas(facturas: List[Factura[Nueva]])(implicit m: Monoid[Money]): Money = 31 | facturas.foldLeft(mzero[Money])(_ |+| _.valor) 32 | 33 | } 34 | 35 | object CuentasContablesServices extends CuentasContablesServices { 36 | 37 | // implicit object USDMonoid extends Monoid[Money] { 38 | // override def zero: Money = Money.zero( CurrencyUnit.USD ) 39 | // override def append( f1: Money, f2: => Money ): Money = f1 plus f2 40 | // } 41 | 42 | def append(f1: Money, f2: => Money): Money = f1 plus f2 43 | def zero(currencyUnit: CurrencyUnit): Money = Money.zero(currencyUnit) 44 | implicit def MoneyMonoid(currencyUnit: CurrencyUnit): Monoid[Money] = Monoid.instance[Money](append, zero(currencyUnit)) 45 | 46 | } -------------------------------------------------------------------------------- /src/main/scala/co/s4n/fp/monadTransformers.scala: -------------------------------------------------------------------------------- 1 | package co.s4n.fp 2 | 3 | import scalaz._ 4 | import scalaz.std.AllInstances._ 5 | import scala.concurrent.ExecutionContext.Implicits.global 6 | import scala.concurrent.Future 7 | 8 | object OptionTDemo { 9 | 10 | def f1: Future[Option[String]] = Future(Some("a")) 11 | def f2: Option[Int] = Some(1) 12 | def f3: Future[Int] = Future(1) 13 | def f4: Option[Future[Int]] = Some(Future(1)) 14 | 15 | // final case class OptionT[F[_], A](run: F[Option[A]]) { 16 | val result: Future[Option[(String, Int, Future[Int], Future[Int])]] = (for { 17 | a <- OptionT[Future, String](f1) 18 | b <- OptionT[Future, Int](Future.successful(f2)) 19 | // Las que siguen son bellas galadas 20 | c <- OptionT[Future, Future[Int]](Future.successful(Some(f3))) 21 | d <- OptionT[Future, Future[Int]](Future.successful(f4)) 22 | } yield (a, b, c, d)).run 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/scala/co/s4n/fp/monoid.scala: -------------------------------------------------------------------------------- 1 | package co.s4n.fp 2 | 3 | /* Binary operations with: 4 | . Closed: given two elements of the same category, the operation delivers en element of the same category. 5 | . Identity: a (+) identity = identity (+) a = a 6 | . Associativity 7 | . Any type with an "add" notion and have an "identity" element could be a monoid: 8 | - Add operation: (A, A) => A 9 | Must be associative: (x, append(y, z)) == append(append(x, y), z ) 10 | - An element zero of type A 11 | */ 12 | 13 | trait Monoide[A] { 14 | def append(f1: A, f2: => A): A 15 | def zero: A 16 | } 17 | 18 | object BooleanMonoid { 19 | 20 | implicit val booleanAndMonoid: Monoide[Boolean] = new Monoide[Boolean] { 21 | override def append(f1: Boolean, f2: => Boolean): Boolean = 22 | f1 && f2 23 | 24 | override def zero: Boolean = true 25 | } 26 | 27 | implicit val booleanOrMonoid: Monoide[Boolean] = new Monoide[Boolean] { 28 | override def append(f1: Boolean, f2: => Boolean): Boolean = 29 | f1 || f2 30 | 31 | override def zero: Boolean = false 32 | } 33 | 34 | implicit val booleanXORMonoid: Monoide[Boolean] = new Monoide[Boolean] { 35 | override def append(f1: Boolean, f2: => Boolean): Boolean = 36 | (f1 && !f2) || (!f1 && f2) 37 | 38 | override def zero: Boolean = false 39 | } 40 | 41 | } 42 | 43 | object SetMonoid { 44 | 45 | implicit def setUnionMonoid[T] = new Monoide[Set[T]] { 46 | override def append(f1: Set[T], f2: => Set[T]): Set[T] = 47 | f1 union f2 48 | 49 | override def zero: Set[T] = Set.empty[T] 50 | } 51 | 52 | } 53 | 54 | object SuperAdder { 55 | import scalaz.Monoid 56 | import scalaz.syntax.monoid._ 57 | 58 | def add[A](items: List[A])(implicit m: Monoid[A]): A = 59 | items.foldLeft(mzero[A]) { _ |+| _ } 60 | 61 | } -------------------------------------------------------------------------------- /src/main/scala/ddd/definitions.scala: -------------------------------------------------------------------------------- 1 | package ddd 2 | 3 | trait Repository[T] { 4 | def add(entity: T): Long 5 | def remove(entity: T): Long 6 | def list(): List[T] 7 | def get(id: Long): T 8 | } 9 | 10 | trait DAO[T] { 11 | def insert(record: T): Long 12 | def delete(record: T): Long 13 | def update(record: T): Long 14 | def retrieve(): List[T] 15 | } 16 | -------------------------------------------------------------------------------- /src/main/scala/ddd/transformers.scala: -------------------------------------------------------------------------------- 1 | package ddd 2 | 3 | trait Transformable[Entity, Record] { 4 | def toRecord(value: Entity): Record 5 | } 6 | 7 | object Transformer { 8 | def toRecord[Entity, Record](entity: Entity)(implicit transformer: Transformable[Entity, Record]): Record = 9 | transformer.toRecord(entity) 10 | } 11 | -------------------------------------------------------------------------------- /src/test/scala/co/s4n/comision/ComisionServicesTest.scala: -------------------------------------------------------------------------------- 1 | package co.s4n.comision 2 | 3 | import co.s4n.comision.infrastructure.{ ClienteRecord, ComisionRecord, ClienteDAO, ComisionDAO } 4 | import org.mockito.Mockito._ 5 | import org.scalatest.FunSuite 6 | import org.scalatest.mock.MockitoSugar 7 | import co.s4n.comision.domain._ 8 | 9 | import scala.util.{ Success, Failure, Try } 10 | 11 | class ComisionServicesTest extends FunSuite with MockitoSugar { 12 | 13 | val c: Comision[Nueva] = new Comision( 14 | id = None, 15 | valorComision = 1l, 16 | iva = 1l, 17 | Nueva(), 18 | new Cliente("CC1234567", "Pepito") 19 | ) 20 | 21 | lazy val comisionRepository = mock[ComisionRepository] 22 | when(comisionRepository.add(c)).thenReturn(7l) 23 | 24 | val services = new ComisionServices() 25 | 26 | test("Create a Comision using DI") { 27 | val creada: Comision[Nueva] = services.crear(c) 28 | info(s"$creada") 29 | assert(creada.id === Some(7l)) 30 | } 31 | 32 | ignore("Typing example") { 33 | // val facturar = ComisionServices.facturar( c ) // <= No compila. 34 | assert(true) 35 | } 36 | 37 | test("Functional composition: flatMap") { 38 | val comisionProcesada: Try[Comision[Facturada]] = 39 | services.liquidar(c) 40 | .flatMap(services.aprobar(_) 41 | .flatMap(services.facturar(_))) 42 | 43 | comisionProcesada match { 44 | case Success(procesada) => assert(procesada.estado == Facturada()) 45 | case Failure(ex) => fail(ex) 46 | } 47 | } 48 | 49 | test("Functional composition: for-comprehension") { 50 | val comisionProcesada: Try[Comision[Facturada]] = for { 51 | liquidada <- services.liquidar(c) 52 | aprobada <- services.aprobar(liquidada) 53 | facturada <- services.facturar(aprobada) 54 | } yield facturada 55 | 56 | comisionProcesada match { 57 | case Success(procesada) => assert(procesada.estado == Facturada()) 58 | case Failure(ex) => fail(ex) 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/test/scala/co/s4n/comision/domain/ComisionRepositoryTest.scala: -------------------------------------------------------------------------------- 1 | package co.s4n.comision.domain 2 | 3 | import co.s4n.comision.infrastructure._ 4 | import org.scalatest.FunSuite 5 | import org.scalatest.mock.MockitoSugar 6 | import org.mockito.Mockito._ 7 | 8 | class ComisionRepositoryTest extends FunSuite with MockitoSugar { 9 | 10 | val c: Comision[Nueva] = new Comision( 11 | id = None, 12 | valorComision = 1l, 13 | iva = 1l, 14 | Nueva(), 15 | new Cliente("", "") 16 | ) 17 | 18 | test("Mocked DI test") { 19 | val comisionDAO = mock[ComisionDAO] 20 | when(comisionDAO.insert(ComisionRecord(None, 1l, 1l, "", ""))).thenReturn(2l) 21 | 22 | val clienteDAO = mock[ClienteDAO] 23 | when(clienteDAO.insert(ClienteRecord("", ""))).thenReturn(2l) 24 | 25 | val repo = new ComisionRepository(comisionDAO, clienteDAO) 26 | val res: Long = repo.add(c) 27 | info(s"res = $res") 28 | assert(2L === res) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/test/scala/co/s4n/comision/domain/ComisionTest.scala: -------------------------------------------------------------------------------- 1 | package co.s4n.comision.domain 2 | 3 | import org.scalatest.FunSuite 4 | 5 | class ComisionTest extends FunSuite { 6 | 7 | val c: Comision[Nueva] = new Comision( 8 | id = None, 9 | valorComision = 1l, 10 | iva = 1l, 11 | Nueva(), 12 | new Cliente("CC1234567", "Pepito") 13 | ) 14 | 15 | test("Comision lens para cambiar Cliente") { 16 | val cliente: String = Comision.responsable.get(c) 17 | assert("Pepito" === cliente) 18 | val comision: Comision[EstadoComision] = Comision.responsable.set(c, "Paquito") 19 | assert("Paquito" === comision.cliente.nombre) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/scala/co/s4n/comision/infrastructure/TransformableTest.scala: -------------------------------------------------------------------------------- 1 | package co.s4n.comision.infrastructure 2 | 3 | import co.s4n.comision.domain.{ Cliente, Nueva, Comision } 4 | import ddd.Transformer 5 | import org.scalatest.FunSuite 6 | 7 | class TransformableTest extends FunSuite { 8 | 9 | val c = new Comision( 10 | id = None, 11 | valorComision = 1l, 12 | iva = 1l, 13 | Nueva(), 14 | new Cliente("CC1234567", "Pepito") 15 | ) 16 | 17 | test("Basic transformation Entity to Record test") { 18 | import co.s4n.comision.domain.ComisionRepository._ 19 | 20 | val record: (ComisionRecord, ClienteRecord) = Transformer.toRecord(c) 21 | assert("CC1234567" === record._2.id) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/scala/co/s4n/factura/FacturaServicesTest.scala: -------------------------------------------------------------------------------- 1 | package co.s4n.factura 2 | 3 | import org.joda.money.Money 4 | import org.joda.time.DateTime 5 | import org.scalatest.FunSuite 6 | 7 | import scala.util.Try 8 | 9 | class FacturaServicesTest extends FunSuite { 10 | 11 | val f = new Factura[Nueva]( 12 | fecha = DateTime.now(), 13 | idCliente = "", 14 | valor = Money.parse("COP 12000"), 15 | iva = 0.16d, 16 | Nueva() 17 | ) 18 | 19 | test("Causar factura") { 20 | import FacturaServices._ 21 | val result: Try[Factura[Causada]] = for { 22 | aprobada <- aprobar(f) 23 | causada <- causar(aprobada) 24 | } yield causada 25 | 26 | result.map { 27 | c => 28 | val reference = math.BigDecimal(13920).bigDecimal 29 | val divide = reference.divide(c.state.valor.getAmount) 30 | assert(1 === divide) 31 | } recover { 32 | case _ => fail() 33 | } 34 | () 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/test/scala/co/s4n/factura/TicketsServiceTest.scala: -------------------------------------------------------------------------------- 1 | package co.s4n.factura 2 | 3 | import co.s4n.factura.TicketsService.Requestable 4 | import org.scalatest.FunSuite 5 | 6 | import scalaz._ 7 | import Scalaz._ 8 | 9 | class TicketsServiceTest extends FunSuite { 10 | import TicketsService._ 11 | 12 | test("flatMap without providing an interpreter, A.K.A. Look mom, no hands!") { 13 | val buy: Requestable[List[Ticket]] = 14 | TicketsService.buy(5).flatMap(discounts) 15 | assert(true) 16 | } 17 | 18 | test("Free Program") { 19 | val program: Requestable[List[Ticket]] = for { 20 | ts <- buy(5) 21 | dts <- discounts(ts) 22 | vdts <- vip(dts) 23 | } yield (vdts) 24 | assert(true) 25 | } 26 | 27 | test("Filter that shit") { 28 | val tks: List[Ticket] = List(Ticket("", "")) 29 | val requestable = filterM[Ticket, Requestable](tks)(isBonus) 30 | assert(true) 31 | } 32 | 33 | test("Sequence that shit") { 34 | val tks = List(Ticket("", "")) 35 | val requests: List[Requestable[List[Ticket]]] = 36 | List(buy(5), discounts(tks)) 37 | val seq: Requestable[List[List[Ticket]]] = requests.sequence 38 | assert(true) 39 | } 40 | 41 | test("Interpreter") { 42 | import co.s4n.factura.OptionInterpreter 43 | import co.s4n.factura.ListInterpreter 44 | 45 | val program: Requestable[List[Ticket]] = for { 46 | ts <- buy(5) 47 | dts <- discounts(ts) 48 | vdts <- vip(dts) 49 | } yield (vdts) 50 | 51 | val res = Free.runFC(program)(OptionInterpreter) 52 | info(s"res = $res") 53 | 54 | val resF = Free.runFC(program)(ListInterpreter) 55 | info(s"resF = $resF") 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/scala/co/s4n/fp/CuentasContablesServicesTest.scala: -------------------------------------------------------------------------------- 1 | package co.s4n.fp 2 | 3 | import co.s4n.factura.{ Nueva, Factura } 4 | import org.joda.money.CurrencyUnit 5 | import org.joda.time.DateTime 6 | import org.scalatest.FunSuite 7 | 8 | import scalaz.Monoid 9 | 10 | class CuentasContablesServicesTest extends FunSuite { 11 | import org.joda.money.Money 12 | 13 | private val cuentasContables = List( 14 | CuentaContable("a", Money.parse("USD 10")), 15 | CuentaContable("a", Money.parse("USD 20")), 16 | CuentaContable("b", Money.parse("USD 30")), 17 | CuentaContable("b", Money.parse("USD 40")), 18 | CuentaContable("c", Money.parse("USD 50")) 19 | ) 20 | 21 | test("Balance del día") { 22 | implicit val r: Monoid[Money] = CuentasContablesServices.MoneyMonoid(CurrencyUnit.USD) 23 | val balance: Money = CuentasContablesServices.balanceDelTipo("a", cuentasContables) 24 | assert(Money.parse("USD 30").isEqual(balance)) 25 | } 26 | 27 | test("Máxima del día") { 28 | implicit val r: Monoid[Money] = CuentasContablesServices.MoneyMonoid(CurrencyUnit.USD) 29 | val maxTipo: Money = CuentasContablesServices.maximaCuentaDelTipo("b", cuentasContables) 30 | assert(Money.parse("USD 40").isEqual(maxTipo)) 31 | } 32 | 33 | test("Sumatoria de facturas") { 34 | implicit val r: Monoid[Money] = CuentasContablesServices.MoneyMonoid(CurrencyUnit.JPY) 35 | val balance: Money = CuentasContablesServices.balanceFacturas(List(new Factura(DateTime.now(), "A", Money.parse("JPY 10"), 0.16, Nueva()))) 36 | assert(Money.parse("JPY 10").isEqual(balance)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/scala/co/s4n/fp/MonoidTest.scala: -------------------------------------------------------------------------------- 1 | package co.s4n.fp 2 | 3 | import org.scalatest.FunSuite 4 | 5 | class MonoidTest extends FunSuite { 6 | 7 | test("Smoke test") { 8 | val str = "a" ++ "b" // Sequence concatenation 9 | assert("ab" === str) 10 | } 11 | 12 | test("Boolean AND monoid") { 13 | import BooleanMonoid._ 14 | assert(booleanAndMonoid.append(true, true)) 15 | } 16 | 17 | test("Boolean OR monoid") { 18 | import BooleanMonoid._ 19 | assert(booleanOrMonoid.append(false, true)) 20 | } 21 | 22 | test("Boolean XOR monoid") { 23 | import BooleanMonoid._ 24 | assert(booleanXORMonoid.append(true, false)) 25 | } 26 | 27 | test("Set of Int monoid") { 28 | import SetMonoid._ 29 | val s1 = Set(1) 30 | val s2 = Set(2) 31 | assert((Set(1, 2)) == setUnionMonoid.append(s1, s2)) 32 | } 33 | 34 | test("Monoid List of Int add") { 35 | import scalaz.std.anyVal._ 36 | 37 | assert(6 === SuperAdder.add(List(1, 2, 3))) 38 | } 39 | 40 | // test( "Monoid List of Option[Int] add" ) { 41 | // assert( 6 === SuperAdder.add( List( Some( 1 ), Some( 2 ), None, Some( 3 ) ) ) ) 42 | // } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/test/scala/co/s4n/fp/OptionTDemoTest.scala: -------------------------------------------------------------------------------- 1 | package co.s4n.fp 2 | 3 | import org.scalatest.FunSuite 4 | import scala.concurrent.{ Await, Future } 5 | import scala.concurrent.duration._ 6 | 7 | class OptionTDemoTest extends FunSuite { 8 | 9 | test("OptionT test") { 10 | val result: Future[Option[(String, Int, Future[Int], Future[Int])]] = OptionTDemo.result 11 | val a: Option[(String, Int, Future[Int], Future[Int])] = Await.result(result, 1.seconds) 12 | a map { 13 | r: (String, Int, Future[Int], Future[Int]) => 14 | val r3: Int = Await.result(r._3, 1.seconds) 15 | val r4: Int = Await.result(r._4, 1.seconds) 16 | assert("a" === r._1) 17 | assert(1 === r._2) 18 | assert(1 === r3) 19 | assert(1 === r4) 20 | } 21 | assert(true) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/test/scala/co/s4n/fp/ReaderMonadDITest.scala: -------------------------------------------------------------------------------- 1 | package co.s4n.fp 2 | 3 | import org.scalatest.FunSuite 4 | import scala.concurrent.Future 5 | import scalaz._ 6 | import scala.concurrent.ExecutionContext.Implicits.global 7 | 8 | class ReaderMonadDITest extends FunSuite { 9 | 10 | test("Repository DI") { 11 | val u1 = new UserServicesClient() with DBUserRepositoryModule 12 | u1.testMethod().map(u => assert(User(Some("DB"), "Test") === u)) 13 | 14 | val u2 = new UserServicesClient() with FileUserRepositoryModule 15 | u2.testMethod().map(u => assert(User(Some("File"), "Test") === u)) 16 | () 17 | } 18 | 19 | } 20 | 21 | case class User(id: Option[String], name: String) 22 | 23 | sealed trait UserRepository { 24 | def get(id: Int): Future[User] 25 | 26 | def add(user: User): Future[User] 27 | } 28 | 29 | class DBUserRepository() extends UserRepository { 30 | override def get(id: Int): Future[User] = Future.successful(User(Some("DB"), "DB")) 31 | 32 | override def add(user: User): Future[User] = Future.successful(user.copy(id = Some("DB"))) 33 | } 34 | 35 | class FileUserRepository() extends UserRepository { 36 | override def get(id: Int): Future[User] = Future.successful(User(Some("File"), "File")) 37 | 38 | override def add(user: User): Future[User] = Future.successful(user.copy(id = Some("File"))) 39 | } 40 | 41 | object UserServices { 42 | 43 | def getUser(id: Int): Reader[UserRepository, Future[User]] = Reader { 44 | (repository: UserRepository) => repository.get(id) 45 | } 46 | 47 | def createUser(name: String): Reader[UserRepository, Future[User]] = Reader { 48 | (repository: UserRepository) => repository.add(User(None, name)) 49 | } 50 | } 51 | 52 | trait UserRepositoryModule { 53 | def repository: UserRepository 54 | } 55 | 56 | trait DBUserRepositoryModule extends UserRepositoryModule { 57 | def repository: UserRepository = new DBUserRepository() 58 | } 59 | 60 | trait FileUserRepositoryModule extends UserRepositoryModule { 61 | def repository: UserRepository = new FileUserRepository() 62 | } 63 | 64 | class UserServicesClient { 65 | this: UserRepositoryModule => 66 | 67 | def testMethod(): Future[User] = { 68 | val ur: Reader[UserRepository, Future[User]] = for { 69 | u1 <- UserServices.createUser("Test") 70 | u2 <- UserServices.getUser(1) 71 | } yield u2 72 | ur.run(repository) 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /src/test/scala/co/s4n/uuid/UUIDGenTest.scala: -------------------------------------------------------------------------------- 1 | package co.s4n.uuid 2 | 3 | import java.nio.ByteBuffer 4 | import org.scalatest.FunSuite 5 | 6 | class UUIDGenTest extends FunSuite { 7 | 8 | test("Verify ordering") { 9 | val one = UUIDGen.getTimeUUID() 10 | val two = UUIDGen.getTimeUUID() 11 | assert(one.timestamp() < two.timestamp()) 12 | } 13 | 14 | test("Decompose and raw") { 15 | val a = UUIDGen.getTimeUUID(); 16 | val decomposed = UUIDGen.decompose(a); 17 | val b = UUIDGen.getUUID(ByteBuffer.wrap(decomposed)); 18 | assert(a.equals(b)) 19 | } 20 | 21 | test("UUIDTimeStamp") { 22 | val now = System.currentTimeMillis() 23 | val uuid = UUIDGen.getTimeUUID() 24 | val tstamp = UUIDGen.getAdjustedTimestamp(uuid) 25 | assert(now <= tstamp) 26 | } 27 | } 28 | --------------------------------------------------------------------------------