Skip to content

Commit

Permalink
Add name based UUIDv5 implementation (#197)
Browse files Browse the repository at this point in the history
Add name based UUIDv5 implementation
  • Loading branch information
ChristopherDavenport authored Dec 10, 2019
2 parents c8c44fb + 194477b commit 255a2bc
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.chrisdavenport.fuuid

import cats.ApplicativeError

object PlatformSpecificMethods {
def nameBased[F[_]]: (FUUID, String, ApplicativeError[F, Throwable]) => F[FUUID] = (_, _, AE) =>
AE.raiseError(new NotImplementedError("Name based UUIDs are not supported for Scala.js"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.chrisdavenport.fuuid

import cats._
import cats.implicits._
import java.util.UUID
import java.security.MessageDigest

object PlatformSpecificMethods {
def nameBased[F[_]]: (FUUID, String, ApplicativeError[F, Throwable]) => F[FUUID] = (namespace, name, AE) =>
Either
.catchNonFatal(
MessageDigest
.getInstance("SHA-1")
.digest(
uuidBytes(namespace) ++
name.getBytes("UTF-8")
)
)
.map { bs =>
val cs = bs.take(16) // Truncate 160 bits (20 bytes) SHA-1 to fit into our 128 bits of space
cs(6) = (cs(6) & 0x0f).asInstanceOf[Byte] /* clear version */
cs(6) = (cs(6) | 0x50).asInstanceOf[Byte] /* set to version 5, SHA1 UUID */
cs(8) = (cs(8) & 0x3f).asInstanceOf[Byte] /* clear variant */
cs(8) = (cs(8) | 0x80).asInstanceOf[Byte] /* set to IETF variant */
cs
}
.flatMap(
bs =>
Either.catchNonFatal {
val bb = java.nio.ByteBuffer.allocate(java.lang.Long.BYTES)
bb.put(bs, 0, 8)
bb.flip
val most = bb.getLong
bb.flip
bb.put(bs, 8, 8)
bb.flip
val least = bb.getLong
FUUID.fromUUID(new UUID(most, least))
}
)
.liftTo[F](AE)

private def uuidBytes(fuuid: FUUID): Array[Byte] = {
val bb = java.nio.ByteBuffer.allocate(2 * java.lang.Long.BYTES)
val uuid = FUUID.Unsafe.toUUID(fuuid)
bb.putLong(uuid.getMostSignificantBits)
bb.putLong(uuid.getLeastSignificantBits)
bb.array
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.chrisdavenport.fuuid

import cats.effect.IO
import org.specs2._

class FUUIDJvmSpec extends mutable.Specification with ScalaCheck {

"FUUID.nameBased" should {
"produce a valid UUID" in {
(for {
namespace <- FUUID.randomFUUID[IO]
namebased <- FUUID.nameBased[IO](namespace, "What up yo!")
parsed <- FUUID.fromStringF[IO](namebased.toString)
} yield parsed)
.attempt
.unsafeRunSync
.isRight must_=== true
}

"conform to an example" in {
val namespace = FUUID.fromStringF[IO]("dc79a6bc-de3c-5bc7-a877-712bea708d8f").unsafeRunSync()
val name = "What up yo!"

val expected = FUUID.fromStringF[IO]("1cce4593-d217-5b33-be0d-2e81462e79d3").unsafeRunSync()

FUUID.nameBased[IO](namespace, name).unsafeRunSync must_=== expected
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ object FUUID {
}
}

/**
* Creates a new name-based UUIDv5.
* NOTE: Not implemented for Scala.js!
**/
def nameBased[F[_]](namespace: FUUID, name: String)(implicit AE: ApplicativeError[F, Throwable]): F[FUUID] =
PlatformSpecificMethods.nameBased[F](namespace, name, AE)

/**
* A Home For functions we don't trust
* Hopefully making it very clear that this code needs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import java.util.UUID
import cats.effect.IO
import org.specs2._

object FUUIDSpec extends mutable.Specification with ScalaCheck {
class FUUIDSpec extends mutable.Specification with ScalaCheck {

"FUUID.fromString" should {
"Fail when parsing an invalid string" in {
Expand Down

0 comments on commit 255a2bc

Please sign in to comment.