From 78dd97d1044373cd207fd1ded7527bf3d9d0f944 Mon Sep 17 00:00:00 2001 From: qwbarch Date: Sat, 24 Sep 2022 02:27:47 -0400 Subject: [PATCH] Wipe git history --- .github/workflows/scala.yml | 17 ++ .gitignore | 9 + .scalafmt.conf | 10 + LICENSE | 2 +- README.md | 25 +- build.sbt | 184 ++++++++++++ .../qwbarch/snowflake4s/circe/syntax.scala | 36 +++ .../snowflake4s/circe/CirceSuite.scala | 44 +++ .../qwbarch/snowflake4s/Snowflake.scala | 62 ++++ .../github/qwbarch/snowflake4s/package.scala | 56 ++++ .../qwbarch/snowflake4s/Snowflake.scala | 67 +++++ .../github/qwbarch/snowflake4s/IdWorker.scala | 135 +++++++++ .../qwbarch/snowflake4s/IdWorkerBuilder.scala | 129 +++++++++ .../qwbarch/snowflake4s/WorkerState.scala | 24 ++ .../qwbarch/snowflake4s/IdWorkerSuite.scala | 267 ++++++++++++++++++ .../qwbarch/snowflake4s/SnowflakeSuite.scala | 34 +++ .../qwbarch/snowflake4s/arbitrary.scala | 31 ++ .../qwbarch/snowflake4s/generator.scala | 29 ++ modules/docs/src/main/paradox/index.md | 75 +++++ .../src/main/paradox/integration/index.md | 68 +++++ .../snowflake4s/http4s/SnowflakeVar.scala | 28 ++ .../qwbarch/snowflake4s/http4s/syntax.scala | 30 ++ .../snowflake4s/http4s/Http4sSuite.scala | 70 +++++ .../qwbarch/snowflake4s/skunk/codec.scala | 30 ++ .../snowflake4s/skunk/SkunkSuite.scala | 38 +++ project/Dependency.scala | 35 +++ project/Version.scala | 13 + project/build.properties | 1 + project/plugins.sbt | 10 + version.sbt | 1 + 30 files changed, 1558 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/scala.yml create mode 100644 .scalafmt.conf create mode 100644 build.sbt create mode 100644 modules/circe/src/main/scala/io/github/qwbarch/snowflake4s/circe/syntax.scala create mode 100644 modules/circe/src/test/scala/io/github/qwbarch/snowflake4s/circe/CirceSuite.scala create mode 100644 modules/core/src/main/scala-2.12/io/github/qwbarch/snowflake4s/Snowflake.scala create mode 100644 modules/core/src/main/scala-2.13/io/github/qwbarch/snowflake4s/package.scala create mode 100644 modules/core/src/main/scala-3/io/github/qwbarch/snowflake4s/Snowflake.scala create mode 100644 modules/core/src/main/scala/io/github/qwbarch/snowflake4s/IdWorker.scala create mode 100644 modules/core/src/main/scala/io/github/qwbarch/snowflake4s/IdWorkerBuilder.scala create mode 100644 modules/core/src/main/scala/io/github/qwbarch/snowflake4s/WorkerState.scala create mode 100644 modules/core/src/test/scala/io/github/qwbarch/snowflake4s/IdWorkerSuite.scala create mode 100644 modules/core/src/test/scala/io/github/qwbarch/snowflake4s/SnowflakeSuite.scala create mode 100644 modules/core/src/test/scala/io/github/qwbarch/snowflake4s/arbitrary.scala create mode 100644 modules/core/src/test/scala/io/github/qwbarch/snowflake4s/generator.scala create mode 100644 modules/docs/src/main/paradox/index.md create mode 100644 modules/docs/src/main/paradox/integration/index.md create mode 100644 modules/http4s/src/main/scala/io/github/qwbarch/snowflake4s/http4s/SnowflakeVar.scala create mode 100644 modules/http4s/src/main/scala/io/github/qwbarch/snowflake4s/http4s/syntax.scala create mode 100644 modules/http4s/src/test/scala/io/github/qwbarch/snowflake4s/http4s/Http4sSuite.scala create mode 100644 modules/skunk/src/main/scala/io/github/qwbarch/snowflake4s/skunk/codec.scala create mode 100644 modules/skunk/src/test/scala/io/github/qwbarch/snowflake4s/skunk/SkunkSuite.scala create mode 100644 project/Dependency.scala create mode 100644 project/Version.scala create mode 100644 project/build.properties create mode 100644 project/plugins.sbt create mode 100644 version.sbt diff --git a/.github/workflows/scala.yml b/.github/workflows/scala.yml new file mode 100644 index 0000000..b0171a6 --- /dev/null +++ b/.github/workflows/scala.yml @@ -0,0 +1,17 @@ +name: Scala CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: olafurpg/setup-scala@v10 + - name: Run tests + run: sbt ci diff --git a/.gitignore b/.gitignore index 9c07d4a..f8ae8ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,11 @@ *.class *.log + +.vscode +.idea +.bloop +.bsp +.metals + +metals.sbt +target diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..9c619f9 --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,10 @@ +version = 3.5.3 +style = default +maxColumn = 120 +align.preset = none +docstrings.blankFirstLine = yes +docstrings.style = Asterisk +docstrings.oneline = unfold +docstrings.wrap = no +trailingCommas = always +runner.dialect = scala3 diff --git a/LICENSE b/LICENSE index 1454581..d82808c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Edward Yang +Copyright (c) 2021 qwbarch Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 03322fb..e9a37dc 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,25 @@ # snowflake4s -Functional snowflake id generator + +[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/qwbarch/snowflake4s/Scala%20CI?logo=github)](https://github.com/qwbarch/snowflake4s/actions/workflows/scala.yml) +[![snowflake4s Scala version support](https://index.scala-lang.org/qwbarch/snowflake4s/snowflake4s/latest-by-scala-version.svg)](https://index.scala-lang.org/qwbarch/snowflake4s/snowflake4s) +[![scaladoc](https://javadoc.io/badge2/io.github.qwbarch/snowflake4s_3/scaladoc.svg)](https://javadoc.io/doc/io.github.qwbarch/snowflake4s_3) +[![license](https://img.shields.io/badge/license-MIT-green)](https://opensource.org/licenses/MIT) +[![Scala Steward badge](https://img.shields.io/badge/Scala_Steward-helping-blue.svg?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAMAAAARSr4IAAAAVFBMVEUAAACHjojlOy5NWlrKzcYRKjGFjIbp293YycuLa3pYY2LSqql4f3pCUFTgSjNodYRmcXUsPD/NTTbjRS+2jomhgnzNc223cGvZS0HaSD0XLjbaSjElhIr+AAAAAXRSTlMAQObYZgAAAHlJREFUCNdNyosOwyAIhWHAQS1Vt7a77/3fcxxdmv0xwmckutAR1nkm4ggbyEcg/wWmlGLDAA3oL50xi6fk5ffZ3E2E3QfZDCcCN2YtbEWZt+Drc6u6rlqv7Uk0LdKqqr5rk2UCRXOk0vmQKGfc94nOJyQjouF9H/wCc9gECEYfONoAAAAASUVORK5CYII=)](https://scala-steward.org) + +Visit the [microsite](https://qwbarch.github.io/snowflake4s/) for more information. + +## Quickstart + +```scala +libraryDependencies += "io.github.qwbarch" %% "snowflake4s" % "1.1.0-RC1" +``` + +## Library integration + +```scala +libraryDependencies ++= Seq( + "io.github.qwbarch" %% "snowflake4s-circe" % "1.1.0-RC1", + "io.github.qwbarch" %% "snowflake4s-skunk" % "1.1.0-RC1", + "io.github.qwbarch" %% "snowflake4s-http4s" % "1.1.0-RC1", +) +``` diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..a8dcdbf --- /dev/null +++ b/build.sbt @@ -0,0 +1,184 @@ +import Dependency._ +import ReleaseTransformations._ + +lazy val commonSettings: Seq[SettingsDefinition] = Seq( + organization := "io.github.qwbarch", + scalaVersion := "3.1.2", + crossScalaVersions := Seq("3.1.2", "2.13.8", "2.12.15"), + testFrameworks += new TestFramework("weaver.framework.CatsEffect"), + libraryDependencies ++= Seq( + log4CatsNoOp % Test, + weaverCats % Test, + weaverScalaCheck % Test, + ), + // Enable Ymacro-annotations for scala 2.13, required for newtypes + scalacOptions ++= { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, 13)) => "-Ymacro-annotations" :: Nil + case _ => Nil + } + }, + // Enable some scala 3 syntax for scala 2.12 and 2.13 + scalacOptions ++= { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, _)) => "-Xsource:3" :: Nil + case _ => Nil + } + }, + // Enable better-monadic-for with non-dotty versions + libraryDependencies ++= { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, _)) => compilerPlugin(betterMonadicFor) :: Nil + case _ => Nil + } + }, + ) + + lazy val root = (project in file(".")) + .settings(commonSettings ++ noPublishSettings ++ releaseSettings: _*) +.settings( + Compile / unmanagedSourceDirectories := Nil, + Test / unmanagedSourceDirectories := Nil, + ) +.aggregate(core, circe, skunk, http4s) + + lazy val core = (project in file("modules/core")) +.settings(commonSettings ++ publishSettings: _*) + .settings( + name := "snowflake4s", + libraryDependencies ++= Seq( + catsCore, + catsKernel, + catsEffectKernel, + log4CatsCore, + ), + // Use newtypes for scala 2.13 + libraryDependencies ++= { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, 13)) => newType :: Nil + case _ => Nil + } + }, + ) + + lazy val circe = (project in file("modules/circe")) + .dependsOn(core % "compile->compile;test->test") +.settings(commonSettings ++ publishSettings: _*) + .settings( + name := "snowflake4s-circe", + libraryDependencies += circeCore, + ) + + lazy val skunk = (project in file("modules/skunk")) + .dependsOn(core % "compile->compile;test->test") +.settings(commonSettings ++ publishSettings: _*) + .settings( + name := "snowflake4s-skunk", + libraryDependencies += skunkCore, + ) + + lazy val http4s = (project in file("modules/http4s")) + .dependsOn(core % "compile->compile;test->test") + .dependsOn(circe % "test->test") +.settings(commonSettings ++ publishSettings: _*) + .settings( + name := "snowflake4s-http4s", + libraryDependencies ++= Seq( + http4sCore, + http4sDsl % Test, + http4sCirce % Test, + ), + ) + + lazy val docs = (project in file("modules/docs")) + .dependsOn(core) + .enablePlugins(ParadoxPlugin) + .enablePlugins(ParadoxSitePlugin) + .enablePlugins(GhpagesPlugin) +.settings(commonSettings ++ noPublishSettings: _*) + .settings( + scalacOptions := Nil, + git.remoteRepo := "git@github.com:qwbarch/snowflake4s.git", + ghpagesNoJekyll := true, + paradoxTheme := Some(builtinParadoxTheme("generic")), + paradoxProperties ++= Map( + "organization" -> organization.value, + "version" -> version.value, + ), + ) + + lazy val publishSettings = + releaseSettings ++ sharedPublishSettings ++ credentialSettings ++ sharedReleaseProcess + + lazy val credentialSettings = Seq( + credentials ++= + (for { + username <- Option(System.getenv().get("SONATYPE_USERNAME")) + password <- Option(System.getenv().get("SONATYPE_PASSWORD")) + } yield Credentials( + "Sonatype Nexus Repository Manager", + "oss.sonatype.org", + username, + password, + )).toSeq, + ) + +lazy val noPublishSettings = Seq( + publish := (()), + publishLocal := (()), + publishArtifact := false, + publish / skip := true, + ) + + lazy val sharedReleaseProcess = Seq( + releaseProcess := Seq[ReleaseStep]( + checkSnapshotDependencies, + inquireVersions, + runClean, + runTest, + setReleaseVersion, + commitReleaseVersion, + tagRelease, + releaseStepCommandAndRemaining("+publishSigned"), + releaseStepCommand("sonatypeBundleRelease"), + setNextVersion, + commitNextVersion, + pushChanges, + ), + ) + + lazy val releaseSettings = Seq( + scmInfo := Some( + ScmInfo( + url("https://github.com/qwbarch/snowflake4s"), + "scm:git:git@github.com:qwbarch/snowflake4s.git", + ), + ), + homepage := Some(url("https://github.com/qwbarch/snowflake4s")), + licenses := Seq( + "MIT" -> url("https://opensource.org/licenses/MIT"), + ), + pomIncludeRepository := { _ => + false + }, +developers := +List( + Developer( + id = "qwbarch", + name = "qwbarch", + email = "qwbarch@gmail.com", + url = url("https://github.com/qwbarch"), + ), + ), + publishMavenStyle := true, + Test / publishArtifact := false, + sonatypeCredentialHost := "s01.oss.sonatype.org", + sonatypeRepository := "https://s01.oss.sonatype.org/service/local", + versionScheme := Some("semver-spec"), + releaseCrossBuild := true, + usePgpKeyHex("32C27C5322CC8C7A353D1642E524CDF123A41CB7"), + ) + +lazy val sharedPublishSettings = Seq(publishTo := sonatypePublishToBundle.value) + + addCommandAlias("ci", "+undeclaredCompileDependenciesTest; +unusedCompileDependenciesTest; scalafmtCheckAll; +test") diff --git a/modules/circe/src/main/scala/io/github/qwbarch/snowflake4s/circe/syntax.scala b/modules/circe/src/main/scala/io/github/qwbarch/snowflake4s/circe/syntax.scala new file mode 100644 index 0000000..23208cf --- /dev/null +++ b/modules/circe/src/main/scala/io/github/qwbarch/snowflake4s/circe/syntax.scala @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2021 qwbarch + * + * 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: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * 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. + */ +package io.github.qwbarch.snowflake4s.circe + +import io.circe.Decoder +import io.circe.Encoder +import io.circe.KeyDecoder +import io.circe.KeyEncoder +import io.github.qwbarch.snowflake4s.Snowflake + +object syntax { + implicit val snowflakeDecoder: Decoder[Snowflake] = Decoder[Long].map(Snowflake.apply) + implicit val snowflakeEncoder: Encoder[Snowflake] = Encoder.encodeLong.contramap(_.value) + + implicit val snowflakeKeyDecoder: KeyDecoder[Snowflake] = KeyDecoder.instance(Snowflake.fromString(_)) + implicit val snowflakeKeyEncoder: KeyEncoder[Snowflake] = KeyEncoder.instance(_.value.toString) +} diff --git a/modules/circe/src/test/scala/io/github/qwbarch/snowflake4s/circe/CirceSuite.scala b/modules/circe/src/test/scala/io/github/qwbarch/snowflake4s/circe/CirceSuite.scala new file mode 100644 index 0000000..32d1819 --- /dev/null +++ b/modules/circe/src/test/scala/io/github/qwbarch/snowflake4s/circe/CirceSuite.scala @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2021 qwbarch + * + * 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: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * 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. + */ +package io.github.qwbarch.snowflake4s.circe + +import weaver.SimpleIOSuite +import weaver.scalacheck.Checkers +import io.circe.syntax._ +import io.github.qwbarch.snowflake4s.arbitrary._ +import io.github.qwbarch.snowflake4s.circe.syntax._ +import io.github.qwbarch.snowflake4s.Snowflake + +object CirceSuite extends SimpleIOSuite with Checkers { + + test("Snowflake serialization") { + forall { (snowflake: Snowflake) => + expect(snowflake.asJson.as[Snowflake].contains(snowflake)) + } + } + + test("Snowflake key serialization") { + forall { (snowflake: Snowflake) => + expect(Map(snowflake -> snowflake).asJson.as[Map[Snowflake, Snowflake]].contains(Map(snowflake -> snowflake))) + } + } +} diff --git a/modules/core/src/main/scala-2.12/io/github/qwbarch/snowflake4s/Snowflake.scala b/modules/core/src/main/scala-2.12/io/github/qwbarch/snowflake4s/Snowflake.scala new file mode 100644 index 0000000..e1e091c --- /dev/null +++ b/modules/core/src/main/scala-2.12/io/github/qwbarch/snowflake4s/Snowflake.scala @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2021 qwbarch + * + * 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: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * 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. + */ +package io.github.qwbarch.snowflake4s +import scala.util.Try +import cats.Show +import cats.kernel.Eq + +/** + * A 64-bit unique identifier with a timestamp. + */ +final class Snowflake(val value: Long) extends AnyVal { + + override def toString(): String = value.toString +} + +object Snowflake { + implicit val showSnowflake: Show[Snowflake] = Show.fromToString + implicit val eqSnowflake: Eq[Snowflake] = Eq.fromUniversalEquals + + /** + * Constructs a new [[Snowflake]]. + * + * @param value The underlying id as a 64-bit integer. + * @return A snowflake type with zero run-time overhead. + */ + def apply(value: Long): Snowflake = new Snowflake(value) + + /** + * Destructure the snowflake for pattern-matching. + * + * @param snowflake The snowflake id to destructure. + * @return An option containing the underlying value. + */ + def unapply(snowflake: Snowflake): Option[Long] = Some(snowflake.value) + + /** + * Constructs a new [[Snowflake]] from a string. + * + * @param string The string to parse into a snowflake. + * @return The snowflake, if the string is a valid long. + */ + def fromString(string: String): Option[Snowflake] = Try(string.toLong).toOption.map(Snowflake.apply) +} diff --git a/modules/core/src/main/scala-2.13/io/github/qwbarch/snowflake4s/package.scala b/modules/core/src/main/scala-2.13/io/github/qwbarch/snowflake4s/package.scala new file mode 100644 index 0000000..49de825 --- /dev/null +++ b/modules/core/src/main/scala-2.13/io/github/qwbarch/snowflake4s/package.scala @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2021 qwbarch + * + * 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: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * 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. + */ +package io.github.qwbarch + +import io.estatico.newtype.macros.newtype +import scala.util.Try +import cats.Show +import cats.kernel.Eq + +package object snowflake4s { + + /** + * A 64-bit unique identifier with a timestamp. + */ + @newtype case class Snowflake(value: Long) + + object Snowflake { + implicit val showSnowflake: Show[Snowflake] = Show.fromToString + implicit val eqSnowflake: Eq[Snowflake] = Eq.fromUniversalEquals + + /** + * Destructure the snowflake for pattern-matching. + * + * @param snowflake The snowflake id to destructure. + * @return An option containing the underlying value. + */ + def unapply(snowflake: Snowflake): Option[Long] = Some(snowflake.value) + + /** + * Constructs a new [[Snowflake]] from a string. + * + * @param string The string to parse into a snowflake. + * @return The snowflake, if the string is a valid long. + */ + def fromString(string: String): Option[Snowflake] = Try(string.toLong).toOption.map(Snowflake.apply) + } +} diff --git a/modules/core/src/main/scala-3/io/github/qwbarch/snowflake4s/Snowflake.scala b/modules/core/src/main/scala-3/io/github/qwbarch/snowflake4s/Snowflake.scala new file mode 100644 index 0000000..0f5f72c --- /dev/null +++ b/modules/core/src/main/scala-3/io/github/qwbarch/snowflake4s/Snowflake.scala @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2021 qwbarch + * + * 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: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * 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. + */ +package io.github.qwbarch.snowflake4s + +import scala.util.Try +import cats.Show +import cats.kernel.Eq + +/** + * A 64-bit unique identifier with a timestamp. + */ +opaque type Snowflake = Long + +object Snowflake: + given showSnowflake: Show[Snowflake] = Show.fromToString + given eqSnowflake: Eq[Snowflake] = Eq.fromUniversalEquals + + /** + * Constructs a new [[Snowflake]]. + * + * @param value The underlying id as a 64-bit integer. + * @return A snowflake type with zero run-time overhead. + */ + def apply(value: Long): Snowflake = value + + /** + * Destructure the snowflake for pattern-matching. + * + * @param snowflake The snowflake id to destructure. + * @return An option containing the underlying value. + */ + def unapply(snowflake: Snowflake): Option[Long] = Some(snowflake) + + /** + * Constructs a new [[Snowflake]] from a string. + * + * @param string The string to parse into a snowflake. + * @return The snowflake, if the string is a valid long. + */ + def fromString(string: String): Option[Snowflake] = Try(string.toLong).toOption + + extension (snowflake: Snowflake) + /** + * Retrieve the underlying value. + * + * @return The snowflake id as a 64-bit integer. + */ + def value: Long = snowflake diff --git a/modules/core/src/main/scala/io/github/qwbarch/snowflake4s/IdWorker.scala b/modules/core/src/main/scala/io/github/qwbarch/snowflake4s/IdWorker.scala new file mode 100644 index 0000000..85da90f --- /dev/null +++ b/modules/core/src/main/scala/io/github/qwbarch/snowflake4s/IdWorker.scala @@ -0,0 +1,135 @@ +/** + * Copyright (c) 2021 qwbarch + * + * 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: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * 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. + * + * This product includes software developed at Twitter (https://twitter.com/). + */ +package io.github.qwbarch.snowflake4s + +import cats.syntax.all._ +import cats.effect.kernel.Sync +import cats.effect.kernel.Ref +import org.typelevel.log4cats.Logger + +/** + * Generates new snowflake ids. Use [[IdWorkerBuilder]] to create workers. + */ +class IdWorker[F[_]: Sync: Logger]( + state: Ref[F, WorkerState], + epoch: Long, + dataCenterId: Long, + workerId: Long, +) { + + import IdWorker._ + + /** + * Calculates the current time in milliseconds. + * + * @return The current time in milliseconds. + */ + protected def currentTimeMillis: F[Long] = Sync[F].delay(System.currentTimeMillis) + + /** + * Busy-loops until the next millisecond, based on the given timestamp.
+ * + * @param lastTimeStamp The timestamp to base the next millisecond off of. + * @return The next timestamp after the busy-looping finishes. + */ + protected def tilNextMillis(lastTimeStamp: Long): F[Long] = currentTimeMillis.iterateWhile(_ <= lastTimeStamp) + + /** + * Extracts the timestamp of a snowflake. + * + * @param snowflake The snowflake to extract from. + * @return The timestamp of the snowflake. + */ + def getTimeStamp(snowflake: Snowflake): Long = epoch + (snowflake.value >> TimeStampLeftShift) + + /** + * Constructs a new [[Snowflake]] from the given timestamp and sequence. + * + * @param timeStamp The timestamp of the snowflake. + * @param sequence The sequence of the snowflake. + */ + def nextIdPure(timeStamp: Long, sequence: Long): Snowflake = + Snowflake( + ((timeStamp - epoch) << TimeStampLeftShift) | + (dataCenterId << DataCenterIdShift) | + (workerId << WorkerIdShift) | + sequence, + ) + + /** + * Updates the timestamp and sequence. + */ + private def updateState(timeStamp: Long, lastTimeStamp: Long, sequence: Long): F[WorkerState] = + if (timeStamp === lastTimeStamp) { + val nextSequence = (sequence + 1) & SequenceMask + val nextTimeStamp = + if (nextSequence === 0L) tilNextMillis(lastTimeStamp) + else timeStamp.pure[F] + nextTimeStamp.map(WorkerState(_, nextSequence)) + } else WorkerState(timeStamp, 0L).pure[F] + + /** + * Verifies the timestamp isn't going backwards. Raises an exception if so. + */ + private def verifyTimeStamp(timeStamp: Long, lastTimeStamp: Long) = + if (timeStamp < lastTimeStamp) + Logger[F].error(show"Clock is moving backwards. Rejecting requests until $lastTimeStamp.") *> + Sync[F].raiseError( + new RuntimeException( + show"Clock moved backwards. Refusing to generate id for ${lastTimeStamp - timeStamp} milliseconds.", + ), + ) + else Sync[F].unit + + /** + * Generates a new snowflake id. + * + * @return A new snowflake id. + */ + val nextId: F[Snowflake] = + state.access.flatMap { case (WorkerState(lastTimeStamp, sequence), setState) => + for { + timeStamp <- currentTimeMillis + _ <- verifyTimeStamp(timeStamp, lastTimeStamp) + nextState <- updateState(timeStamp, lastTimeStamp, sequence) + successful <- setState(nextState) + id <- + if (successful) nextIdPure(nextState.lastTimeStamp, nextState.sequence).pure[F] + else nextId + } yield id + } +} + +object IdWorker { + final val WorkerIdBits = 5L + final val DataCenterIdBits = 5L + final val MaxWorkerId = -1L ^ (-1L << WorkerIdBits) + final val MaxDataCenterId = -1L ^ (-1L << DataCenterIdBits) + final val SequenceBits = 12L + final val WorkerIdShift = SequenceBits + final val DataCenterIdShift = SequenceBits + WorkerIdBits + final val TimeStampLeftShift = SequenceBits + WorkerIdBits + DataCenterIdBits + final val SequenceMask = -1L ^ (-1L << SequenceBits) + final val TwitterEpoch = 1288834974657L +} diff --git a/modules/core/src/main/scala/io/github/qwbarch/snowflake4s/IdWorkerBuilder.scala b/modules/core/src/main/scala/io/github/qwbarch/snowflake4s/IdWorkerBuilder.scala new file mode 100644 index 0000000..a7d01d8 --- /dev/null +++ b/modules/core/src/main/scala/io/github/qwbarch/snowflake4s/IdWorkerBuilder.scala @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2021 qwbarch + * + * 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: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * 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. + */ +package io.github.qwbarch.snowflake4s + +import scala.util.hashing.MurmurHash3 +import org.typelevel.log4cats.Logger +import cats.syntax.all._ +import cats.effect.kernel.Async +import cats.effect.kernel.Ref + +/** + * A builder for creating instances of [[IdWorker]]. + */ +final class IdWorkerBuilder[F[_]: Async: Logger]( + workerId: Long, + dataCenterId: Long, + epoch: Long, + sequence: Long, +) { + + import IdWorker._ + + private def copy( + workerId: Long = this.workerId, + dataCenterId: Long = this.dataCenterId, + epoch: Long = this.epoch, + sequence: Long = this.sequence, + ): IdWorkerBuilder[F] = new IdWorkerBuilder(workerId, dataCenterId, epoch, sequence) + + override def hashCode: Int = { + var hash = IdWorkerBuilder.hashSeed + hash = MurmurHash3.mix(hash, workerId.##) + hash = MurmurHash3.mix(hash, dataCenterId.##) + hash = MurmurHash3.mix(hash, epoch.##) + hash = MurmurHash3.mixLast(hash, sequence.##) + hash + } + + override def toString: String = show"IdWorkerBuilder($workerId, $dataCenterId, $epoch, $sequence)" + + /** + * Sets the worker's id. + * + * @param workerId The worker id. + * @return A new builder with the provided worker id. + */ + def withWorkerId(workerId: Long): IdWorkerBuilder[F] = copy(workerId = workerId) + + /** + * Sets the worker's data center id. + * + * @param dataCenterId The data center id. + * @return A new builder with the provided data center id. + */ + def withDataCenterId(dataCenterId: Long): IdWorkerBuilder[F] = copy(dataCenterId = dataCenterId) + + /** + * Sets the epoch used for generating ids. + * + * @param epoch The epoch. + * @return A new builder with the provided epoch. + */ + def withEpoch(epoch: Long): IdWorkerBuilder[F] = copy(epoch = epoch) + + /** + * Sets the sequence id. + * + * @param sequence The sequence. + * @return A new sequence with the provided sequence. + */ + def withSequence(sequence: Long): IdWorkerBuilder[F] = copy(sequence = sequence) + + /** + * Creates a new id worker using the builder's arguments. + * + * @return An id worker with the provided builder arguments. + */ + def build: F[IdWorker[F]] = + for { + _ <- Async[F].unit + .ensure( + new IllegalArgumentException(show"Worker id can't be greater than $MaxWorkerId or less than 0."), + )(_ => workerId <= MaxWorkerId && workerId >= 0) + .ensure( + new IllegalArgumentException(show"Data center id can't be greater than $MaxDataCenterId or less than 0."), + )(_ => dataCenterId <= MaxDataCenterId && dataCenterId >= 0) + _ <- Logger[F].info( + show"Worker starting. Timestamp left shift $TimeStampLeftShift, " + + show"data center id bits $DataCenterIdBits, worker id bits $WorkerIdBits, " + + show"sequence bits $SequenceBits, worker id $workerId.", + ) + state <- Ref[F].of(WorkerState(lastTimeStamp = -1, sequence = sequence)) + } yield new IdWorker(state, epoch, dataCenterId, workerId) +} + +object IdWorkerBuilder { + private val hashSeed = MurmurHash3.stringHash("IdWorkerBuilder") + + /** + * The default builder arguments. + * + * @return A new builder with the default arguments. + */ + def default[F[_]: Async: Logger]: IdWorkerBuilder[F] = new IdWorkerBuilder( + workerId = 0, + dataCenterId = 0, + sequence = 0, + epoch = IdWorker.TwitterEpoch, + ) +} diff --git a/modules/core/src/main/scala/io/github/qwbarch/snowflake4s/WorkerState.scala b/modules/core/src/main/scala/io/github/qwbarch/snowflake4s/WorkerState.scala new file mode 100644 index 0000000..1265913 --- /dev/null +++ b/modules/core/src/main/scala/io/github/qwbarch/snowflake4s/WorkerState.scala @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2021 qwbarch + * + * 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: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * 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. + */ +package io.github.qwbarch.snowflake4s + +private[snowflake4s] final case class WorkerState(lastTimeStamp: Long, sequence: Long) diff --git a/modules/core/src/test/scala/io/github/qwbarch/snowflake4s/IdWorkerSuite.scala b/modules/core/src/test/scala/io/github/qwbarch/snowflake4s/IdWorkerSuite.scala new file mode 100644 index 0000000..728d09e --- /dev/null +++ b/modules/core/src/test/scala/io/github/qwbarch/snowflake4s/IdWorkerSuite.scala @@ -0,0 +1,267 @@ +/** + * Copyright (c) 2021 qwbarch + * + * 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: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * 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. + * + * This product includes software developed at Twitter (https://twitter.com/). + */ +package io.github.qwbarch.snowflake4s + +import weaver.SimpleIOSuite +import weaver.scalacheck.Checkers +import org.typelevel.log4cats.Logger +import org.typelevel.log4cats.noop.NoOpLogger +import cats.effect.IO +import cats.effect.kernel.Ref +import cats.syntax.all._ +import io.github.qwbarch.snowflake4s.arbitrary._ +import cats.effect.kernel.Async + +private class EasyTimeWorker[F[_]: Async: Logger]( + val nextMillis: Ref[F, F[Long]], + state: Ref[F, WorkerState], + val epoch: Long, + dataCenterId: Long, + workerId: Long, +) extends IdWorker[F](state, epoch, dataCenterId, workerId) { + + override protected def currentTimeMillis: F[Long] = nextMillis.get.flatten +} + +private class WakingIdWorker[F[_]: Async: Logger]( + nextMillis: Ref[F, F[Long]], + val slept: Ref[F, Int], + val state: Ref[F, WorkerState], + epoch: Long, + dataCenterId: Long, + workerId: Long, +) extends EasyTimeWorker[F](nextMillis, state, epoch, dataCenterId, workerId) { + + override protected def tilNextMillis(lastTimeStamp: Long): F[Long] = + slept.update(_ + 1) *> super.tilNextMillis(lastTimeStamp) +} + +private class StaticTimeWorker[F[_]: Async: Logger]( + val time: Ref[F, Long], + val state: Ref[F, WorkerState], + epoch: Long, + dataCenterId: Long, + workerId: Long, +) extends IdWorker[F](state, epoch, dataCenterId, workerId) { + + override protected def currentTimeMillis: F[Long] = time.get.map(_ + epoch) +} + +object IdWorkerSuite extends SimpleIOSuite with Checkers { + + private final val WorkerMask = 0x000000000001f000L + private final val DataCenterMask = 0x00000000003e0000L + private final val TimeStampMask = 0xffffffffffc00000L + + private implicit val logger: Logger[IO] = NoOpLogger[IO] + + private def createEasyTimeWorker(workerId: Long, dataCenterId: Long) = + for { + state <- Ref.of(WorkerState(lastTimeStamp = -1, sequence = 0L)) + nextMillis <- Ref.of(IO(System.currentTimeMillis)) + } yield new EasyTimeWorker( + nextMillis, + state, + IdWorker.TwitterEpoch, + dataCenterId, + workerId, + ) + + private def createWakingIdWorker(workerId: Long, dataCenterId: Long) = + for { + nextMillis <- Ref.of(IO(System.currentTimeMillis)) + state <- Ref.of(WorkerState(lastTimeStamp = -1, sequence = 0L)) + slept <- Ref.of(0) + } yield new WakingIdWorker( + nextMillis, + slept, + state, + IdWorker.TwitterEpoch, + dataCenterId, + workerId, + ) + + private def createStaticTimeWorker(workerId: Long, dataCenterId: Long) = + for { + state <- Ref[F].of(WorkerState(lastTimeStamp = -1, sequence = 0L)) + time <- Ref.of(1L) + } yield new StaticTimeWorker( + time, + state, + IdWorker.TwitterEpoch, + dataCenterId, + workerId, + ) + + test("Generate id") { + forall { (workerId: Long, dataCenterId: Long) => + for { + worker <- IdWorkerBuilder + .default[IO] + .withWorkerId(workerId) + .withDataCenterId(dataCenterId) + .build + id <- worker.nextId + } yield expect(id.value > 0L) + } + } + + test("Mask worker id") { + forall { (workerId: Long, dataCenterId: Long) => + for { + worker <- IdWorkerBuilder + .default[IO] + .withWorkerId(workerId) + .withDataCenterId(dataCenterId) + .build + id <- worker.nextId + } yield expect.same((id.value & WorkerMask) >> 12, workerId) + } + } + + test("Mask datacenter id") { + forall { (workerId: Long, dataCenterId: Long) => + for { + worker <- IdWorkerBuilder + .default[IO] + .withWorkerId(workerId) + .withDataCenterId(dataCenterId) + .build + id <- worker.nextId + } yield expect((id.value & DataCenterMask) >> 17L == dataCenterId) + } + } + + test("Mask timestamp") { + forall { (workerId: Long, dataCenterId: Long) => + for { + worker <- createEasyTimeWorker(workerId, dataCenterId) + timeStamp <- IO(System.currentTimeMillis) + _ <- worker.nextMillis.set(IO.pure(timeStamp)) + id <- worker.nextId + } yield expect((id.value & TimeStampMask) >> 22L == timeStamp - worker.epoch) + } + } + + test("Id always increases") { + forall { (workerId: Long, dataCenterId: Long) => + for { + worker <- IdWorkerBuilder + .default[IO] + .withWorkerId(workerId) + .withDataCenterId(dataCenterId) + .build + ids <- (1 to 100).map(_ => worker.nextId).toList.sequence + (compareIds, _) = ids + .foldLeft(success -> 0L) { case ((accumulator, previousId), nextId) => + ((expect(nextId.value > previousId) && accumulator), nextId.value) + } + } yield compareIds + } + } + + test("Sleep if rollover twice in a millisecond") { + forall { (workerId: Long, dataCenterId: Long) => + for { + worker <- createWakingIdWorker(workerId, dataCenterId) + iterator = List(2L, 2L, 3L).iterator + _ <- worker.nextMillis.set(IO(iterator.next())) + _ <- worker.state.update(_.copy(sequence = 4095)) + _ <- worker.nextId + _ <- worker.state.update(_.copy(sequence = 4095)) + _ <- worker.nextId + slept <- worker.slept.get + } yield expect.same(1, slept) + } + } + + test("Ids must be unique generating sequentially") { + forall { (workerId: Long, dataCenterId: Long) => + for { + worker <- createWakingIdWorker(workerId, dataCenterId) + ids <- (1 to 100).map(_ => worker.nextId).toList.sequence + } yield expect.same(100, ids.distinct.size) + } + } + + test("Ids must be unique generating in parallel") { + forall { (workerId: Long, dataCenterId: Long) => + for { + worker <- IdWorkerBuilder + .default[IO] + .withWorkerId(workerId) + .withDataCenterId(dataCenterId) + .build + ids <- (0 to 20000).toList.map(_ => worker.nextId).parSequence + distinct = ids.distinct + } yield expect.same(ids.length, distinct.length) + } + } + + test("Ids must be unique even when clock moves backwards") { + forall { (workerId: Long, dataCenterId: Long) => + for { + worker <- createStaticTimeWorker(workerId, dataCenterId) + sequenceMask = -1L ^ (-1L << 12) + + // Reported at https://github.com/twitter/snowflake/issues/6 + // First we generate 2 ids with the same time, so that we get the sequence to 1 + a <- worker.state.get.map(it => expect.same(0, it.sequence)) + b <- worker.time.get.map(it => expect.same(1, it)) + id1 <- worker.nextId + c = expect.same(1, id1.value >> 22) && expect.same(0, id1.value & sequenceMask) + + d <- worker.state.get.map(it => expect.same(0, it.sequence)) + e <- worker.time.get.map(it => expect.same(1, it)) + id2 <- worker.nextId + f = expect.same(1, id2.value >> 22) && expect.same(1, id2.value & sequenceMask) + + // Set time backwards + _ <- worker.time.set(0L) + g <- worker.state.get.map(it => expect.same(1, it.sequence)) + h <- worker.nextId.attempt.map(it => expect(it.isLeft)) + i <- worker.state.get.map(it => expect.same(1, it.sequence)) + + _ <- worker.time.set(1L) + id3 <- worker.nextId + j = expect.same(1, id3.value >> 22) && expect.same(2, id3.value & sequenceMask) + } yield a && b && c && d && e && f && g && h && i && j + } + } + + test("Extract timestamp from id") { + forall { (workerId: Long, dataCenterId: Long) => + for { + worker <- IdWorkerBuilder + .default[IO] + .withWorkerId(workerId) + .withDataCenterId(dataCenterId) + .build + timeStamp <- IO(System.currentTimeMillis) + id = worker.nextIdPure(timeStamp, 0) + timeStamp2 = worker.getTimeStamp(id) + } yield expect.same(timeStamp, timeStamp2) + } + } +} diff --git a/modules/core/src/test/scala/io/github/qwbarch/snowflake4s/SnowflakeSuite.scala b/modules/core/src/test/scala/io/github/qwbarch/snowflake4s/SnowflakeSuite.scala new file mode 100644 index 0000000..c5f496a --- /dev/null +++ b/modules/core/src/test/scala/io/github/qwbarch/snowflake4s/SnowflakeSuite.scala @@ -0,0 +1,34 @@ +package io.github.qwbarch.snowflake4s + +import weaver.SimpleIOSuite +import weaver.scalacheck.Checkers +import io.github.qwbarch.snowflake4s.arbitrary._ +import cats.syntax.all._ + +object SnowflakeSuite extends SimpleIOSuite with Checkers { + + test("Snowflake.toString") { + forall { (snowflake: Snowflake) => + expect.same(snowflake.value.toString, snowflake.toString) + } + } + + test("Snowflake.show") { + forall { (snowflake: Snowflake) => + expect.same(snowflake.toString, snowflake.show) + } + } + + test("Snowflake.##") { + forall { (snowflake: Snowflake) => + expect.same(snowflake.value.##, snowflake.##) + } + } + + test("Snowflake.equals") { + forall { (snowflake: Snowflake) => + val copy = Snowflake(snowflake.value) + expect.same(snowflake, copy) + } + } +} diff --git a/modules/core/src/test/scala/io/github/qwbarch/snowflake4s/arbitrary.scala b/modules/core/src/test/scala/io/github/qwbarch/snowflake4s/arbitrary.scala new file mode 100644 index 0000000..6728fe4 --- /dev/null +++ b/modules/core/src/test/scala/io/github/qwbarch/snowflake4s/arbitrary.scala @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021 qwbarch + * + * 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: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * 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. + */ +package io.github.qwbarch.snowflake4s + +import org.scalacheck.Arbitrary +import io.github.qwbarch.snowflake4s.generator.workerDataCenterIdGen +import io.github.qwbarch.snowflake4s.generator.snowflakeGen + +object arbitrary { + implicit val workerDataCenterIdArbitrary: Arbitrary[Long] = Arbitrary(workerDataCenterIdGen) + implicit val snowflakeArbitrary: Arbitrary[Snowflake] = Arbitrary(snowflakeGen) +} diff --git a/modules/core/src/test/scala/io/github/qwbarch/snowflake4s/generator.scala b/modules/core/src/test/scala/io/github/qwbarch/snowflake4s/generator.scala new file mode 100644 index 0000000..a8063f6 --- /dev/null +++ b/modules/core/src/test/scala/io/github/qwbarch/snowflake4s/generator.scala @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2021 qwbarch + * + * 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: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * 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. + */ +package io.github.qwbarch.snowflake4s + +import org.scalacheck.Gen + +object generator { + val workerDataCenterIdGen: Gen[Long] = Gen.choose(0L, IdWorker.MaxWorkerId) + val snowflakeGen: Gen[Snowflake] = Gen.long.map(Snowflake.apply) +} diff --git a/modules/docs/src/main/paradox/index.md b/modules/docs/src/main/paradox/index.md new file mode 100644 index 0000000..2f4750d --- /dev/null +++ b/modules/docs/src/main/paradox/index.md @@ -0,0 +1,75 @@ +@@@ index + +* [Integration](integration/index.md) + +@@@ + +# snowflake4s + +## Overview + +[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/qwbarch/snowflake4s/Scala%20CI?logo=github)](https://github.com/qwbarch/snowflake4s/actions/workflows/scala.yml) +[![Maven Central](https://img.shields.io/maven-central/v/io.github.qwbarch/snowflake4s_3)](https://search.maven.org/artifact/io.github.qwbarch/snowflake4s_3/1.0.0/jar) +[![scaladoc](https://javadoc.io/badge2/io.github.qwbarch/snowflake4s_3/scaladoc.svg)](https://javadoc.io/doc/io.github.qwbarch/snowflake4s_3) +[![license](https://img.shields.io/badge/license-MIT-green)](https://opensource.org/licenses/MIT) +[![Scala Steward badge](https://img.shields.io/badge/Scala_Steward-helping-blue.svg?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAMAAAARSr4IAAAAVFBMVEUAAACHjojlOy5NWlrKzcYRKjGFjIbp293YycuLa3pYY2LSqql4f3pCUFTgSjNodYRmcXUsPD/NTTbjRS+2jomhgnzNc223cGvZS0HaSD0XLjbaSjElhIr+AAAAAXRSTlMAQObYZgAAAHlJREFUCNdNyosOwyAIhWHAQS1Vt7a77/3fcxxdmv0xwmckutAR1nkm4ggbyEcg/wWmlGLDAA3oL50xi6fk5ffZ3E2E3QfZDCcCN2YtbEWZt+Drc6u6rlqv7Uk0LdKqqr5rk2UCRXOk0vmQKGfc94nOJyQjouF9H/wCc9gECEYfONoAAAAASUVORK5CYII=)](https://scala-steward.org) + +snowflake4s is a purely functional library used for generating unique ids, using Twitter's snowflake id format. + +@@dependency[sbt,Maven,Gradle] { + group="$organization$" + artifact="snowflake4s_$scala.binary.version$" + version="$version$" +} + +## Quick example + +Here is a minimal example to create a snowflake id (Scala 3): + +```scala +import cats.syntax.all.given +import cats.effect.IO +import cats.effect.IOApp +import cats.effect.std.Console +import org.typelevel.log4cats.Logger +import org.typelevel.log4cats.slf4j.Slf4jLogger +import io.github.qwbarch.snowflake4s.IdWorkerBuilder + +object Main extends IOApp.Simple: + override def run: IO[Unit] = + given Logger[IO] = Slf4jLogger.getLogger[IO] + for + idWorker <- IdWorkerBuilder.default[IO].build + id <- idWorker.nextId + _ <- Console[IO].println(id) + yield () +``` + +## Customizing id workers + +A custom epoch, sequence, worker id, and data center id can be provided through the ``IdWorkerBuilder``. + +```scala +IdWorkerBuilder.default[IO] + .withWorkerId(0) + .withDataCenterId(0) + .withEpoch(IdWorker.TwitterEpoch) + .withSequence(0) + .build +``` + +## Running tests + +To run snowflake4s' unit tests, simply enter the following command: + +``` +sbt test +``` + +## Alternative libraries +- [scala-id-generator](https://github.com/softwaremill/scala-id-generator) - If you're looking to generate snowflake ids imperatively, try out this library from lightbend! +- [fuuid](https://github.com/davenverse/fuuid) - Looking for something more lightweight than snowflakes? This is a purely functional library for generating UUID's. + +## Credits + +Full credits goes to [Twitter](https://about.twitter.com/). Implementation is from Twitter's [archives](https://github.com/twitter-archive/snowflake/blob/updated_deps/src/main/scala/com/twitter/service/snowflake/IdWorker.scala). diff --git a/modules/docs/src/main/paradox/integration/index.md b/modules/docs/src/main/paradox/integration/index.md new file mode 100644 index 0000000..279e306 --- /dev/null +++ b/modules/docs/src/main/paradox/integration/index.md @@ -0,0 +1,68 @@ +# Library Integration + +This section shows examples on how to integrate snowflake4s with other libraries. + +## Circe + +@@dependency[sbt,Maven,Gradle] { + group="$organization$" + artifact="snowflake4s-circe_$scala.binary.version$" + version="$version$" +} + +The circe module provides an encoder/decoder for snowflakes. + +```scala +import io.circe.syntax.given +import io.github.qwbarch.snowflake4s.circe.syntax.given +import io.github.qwbarch.snowflake4s.Snowflake + +val snowflake = Snowflake(123456789L) +val encoded = snowflake.asJson +val decoded = encoded.as[Snowflake] +``` + +## Skunk + +@@dependency[sbt,Maven,Gradle] { + group="$organization$" + artifact="snowflake4s-skunk_$scala.binary.version$" + version="$version$" +} + +The skunk module provides a codec for snowflakes. + +```scala +import skunk.Command +import skunk.syntax.all.sql +import io.github.qwbarch.snowflake4s.skunk.codec.snowflake + +val command: Command[Snowflake] = sql"DELETE FROM messages WHERE id=$snowflake".command +``` + +## Http4s + +@@dependency[sbt,Maven,Gradle] { + group="$organization$" + artifact="snowflake4s-http4s_$scala.binary.version$" + version="$version$" +} + +The http4s module provides an extractor for handling path parameters, as well as an implicit required +for a query parameter matcher. + +```scala +import org.http4s.dsl.io.* +import org.http4s.HttpRoutes +import cats.effect.IO +import cats.syntax.all.given +import io.github.qwbarch.snowflake4s.http4s.syntax.given +import io.github.qwbarch.snowflake4s.Snowflake + +object SnowflakeQueryParamMatcher extends QueryParamDecoderMatcher[Snowflake]("id") + +val routes: HttpRoutes[IO] = HttpRoutes.of[IO] { + case GET -> Root / SnowflakeVar(snowflake) => Ok(snowflake.show) + case GET -> Root / "user" :? SnowflakeQueryParamMatcher(snowflake) => Ok(snowflake.show) +} +``` diff --git a/modules/http4s/src/main/scala/io/github/qwbarch/snowflake4s/http4s/SnowflakeVar.scala b/modules/http4s/src/main/scala/io/github/qwbarch/snowflake4s/http4s/SnowflakeVar.scala new file mode 100644 index 0000000..86a2b7b --- /dev/null +++ b/modules/http4s/src/main/scala/io/github/qwbarch/snowflake4s/http4s/SnowflakeVar.scala @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2021 qwbarch + * + * 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: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * 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. + */ +package io.github.qwbarch.snowflake4s.http4s + +import io.github.qwbarch.snowflake4s.Snowflake + +object SnowflakeVar { + def unapply(string: String): Option[Snowflake] = Snowflake.fromString(string) +} diff --git a/modules/http4s/src/main/scala/io/github/qwbarch/snowflake4s/http4s/syntax.scala b/modules/http4s/src/main/scala/io/github/qwbarch/snowflake4s/http4s/syntax.scala new file mode 100644 index 0000000..82d6056 --- /dev/null +++ b/modules/http4s/src/main/scala/io/github/qwbarch/snowflake4s/http4s/syntax.scala @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2021 qwbarch + * + * 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: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * 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. + */ +package io.github.qwbarch.snowflake4s.http4s + +import org.http4s.QueryParamDecoder +import io.github.qwbarch.snowflake4s.Snowflake + +object syntax { + + implicit val snowflakeQueryParamDecoder: QueryParamDecoder[Snowflake] = QueryParamDecoder[Long].map(Snowflake.apply) +} diff --git a/modules/http4s/src/test/scala/io/github/qwbarch/snowflake4s/http4s/Http4sSuite.scala b/modules/http4s/src/test/scala/io/github/qwbarch/snowflake4s/http4s/Http4sSuite.scala new file mode 100644 index 0000000..f2bf93f --- /dev/null +++ b/modules/http4s/src/test/scala/io/github/qwbarch/snowflake4s/http4s/Http4sSuite.scala @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2021 qwbarch + * + * 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: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * 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. + */ +package io.github.qwbarch.snowflake4s.http4s + +import weaver.SimpleIOSuite +import weaver.scalacheck.Checkers +import io.github.qwbarch.snowflake4s.http4s.syntax._ +import io.github.qwbarch.snowflake4s.Snowflake +import io.github.qwbarch.snowflake4s.arbitrary._ +import io.github.qwbarch.snowflake4s.circe.syntax._ +import cats.effect.IO +import cats.syntax.all._ +import org.http4s.dsl.io._ +import org.http4s.Uri +import org.http4s.HttpRoutes +import org.http4s.Request +import org.http4s.dsl.impl.QueryParamDecoderMatcher +import org.http4s.circe.CirceEntityCodec._ + +object Http4sSuite extends SimpleIOSuite with Checkers { + + private object SnowflakeQueryParamMatcher extends QueryParamDecoderMatcher[Snowflake]("id") + + test("Snowflake var pattern matching") { + forall { (snowflake: Snowflake) => + val routes = HttpRoutes.of[IO] { case GET -> Root / SnowflakeVar(snowflake) => Ok(snowflake) } + routes + .run(Request(method = GET, uri = Uri.unsafeFromString(show"/$snowflake"))) + .value + .flatMap { + case Some(response) => response.as[Snowflake].map(expect.same(snowflake, _)) + case None => failure("Unknown route.").pure + } + } + } + + test("Snowflake query param matching") { + forall { (snowflake: Snowflake) => + val routes = HttpRoutes.of[IO] { case GET -> Root / "entity" :? SnowflakeQueryParamMatcher(snowflake) => + Ok(snowflake) + } + routes + .run(Request(method = GET, uri = Uri.unsafeFromString(show"/entity?id=$snowflake"))) + .value + .flatMap { + case Some(response) => response.as[Snowflake].map(expect.same(snowflake, _)) + case None => failure("Unknown route.").pure + } + } + } +} diff --git a/modules/skunk/src/main/scala/io/github/qwbarch/snowflake4s/skunk/codec.scala b/modules/skunk/src/main/scala/io/github/qwbarch/snowflake4s/skunk/codec.scala new file mode 100644 index 0000000..0361630 --- /dev/null +++ b/modules/skunk/src/main/scala/io/github/qwbarch/snowflake4s/skunk/codec.scala @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2021 qwbarch + * + * 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: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * 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. + */ +package io.github.qwbarch.snowflake4s.skunk + +import _root_.skunk.Codec +import _root_.skunk.codec.all.int8 +import io.github.qwbarch.snowflake4s.Snowflake + +object codec { + val snowflake: Codec[Snowflake] = int8.imap(Snowflake.apply)(_.value) +} diff --git a/modules/skunk/src/test/scala/io/github/qwbarch/snowflake4s/skunk/SkunkSuite.scala b/modules/skunk/src/test/scala/io/github/qwbarch/snowflake4s/skunk/SkunkSuite.scala new file mode 100644 index 0000000..9a1b00d --- /dev/null +++ b/modules/skunk/src/test/scala/io/github/qwbarch/snowflake4s/skunk/SkunkSuite.scala @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021 qwbarch + * + * 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: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * 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. + */ +package io.github.qwbarch.snowflake4s.skunk + +import weaver.SimpleIOSuite +import weaver.scalacheck.Checkers +import io.github.qwbarch.snowflake4s.Snowflake +import io.github.qwbarch.snowflake4s.arbitrary._ +import cats.syntax.all._ + +object SkunkSuite extends SimpleIOSuite with Checkers { + + test("Snowflake codec") { + forall { (snowflake: Snowflake) => + expect.same(snowflake.show.some :: Nil, codec.snowflake.encode(snowflake)) && + expect.same(Right(snowflake), codec.snowflake.decode(0, snowflake.value.show.some :: Nil)) + } + } +} diff --git a/project/Dependency.scala b/project/Dependency.scala new file mode 100644 index 0000000..b53e87c --- /dev/null +++ b/project/Dependency.scala @@ -0,0 +1,35 @@ +import sbt._ + +object Dependency { + + private def dependency(group: String)(version: String)(useScalaVersion: Boolean)(artifact: String) = + (if (useScalaVersion) group %% _ else group % _)(artifact) % version + + private val typeLevel = dependency("org.typelevel") _ + val catsCore = typeLevel(Version.catsCore)(true)("cats-core") + val catsKernel = typeLevel(Version.catsKernel)(true)("cats-kernel") + + private val catsEffect = typeLevel(Version.catsEffect)(true) + val catsEffectCore = catsEffect("cats-effect") + val catsEffectStd = catsEffect("cats-effect-std") + val catsEffectKernel = catsEffect("cats-effect-kernel") + + private val log4Cats = typeLevel(Version.log4Cats)(true) + val log4CatsCore = log4Cats("log4cats-core") + val log4CatsNoOp = log4Cats("log4cats-noop") + + private val weaver = dependency("com.disneystreaming")(Version.weaver)(true) _ + val weaverCats = weaver("weaver-cats") + val weaverScalaCheck = weaver("weaver-scalacheck") + + private val http4s = dependency("org.http4s")(Version.http4s)(true) _ + val http4sCore = http4s("http4s-core") + val http4sDsl = http4s("http4s-dsl") + val http4sCirce = http4s("http4s-circe") + + val macroParadise = "org.scalamacros" % "paradise" % Version.macroParadise + val newType = "io.estatico" %% "newtype" % Version.newType + val circeCore = "io.circe" %% "circe-core" % Version.circe + val skunkCore = "org.tpolecat" %% "skunk-core" % Version.skunk + val betterMonadicFor = "com.olegpy" %% "better-monadic-for" % Version.betterMonadicFor +} diff --git a/project/Version.scala b/project/Version.scala new file mode 100644 index 0000000..aeb00f2 --- /dev/null +++ b/project/Version.scala @@ -0,0 +1,13 @@ +object Version { + val betterMonadicFor = "0.3.1" + val catsCore = "2.7.0" + val catsEffect = "3.3.11" + val catsKernel = "2.7.0" + val circe = "0.14.2" + val log4Cats = "2.3.0" + val weaver = "0.7.11" + val macroParadise = "2.1.1" + val newType = "0.4.4" + val skunk = "0.3.1" + val http4s = "1.0.0-M30" +} diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..c8fcab5 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.6.2 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..b71e9ae --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,10 @@ +addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.3.1") +addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.2.16") +addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.9.2") +addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "1.4.1") +addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.6.3") +addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.12") +addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2") +addSbtPlugin("com.github.sbt" % "sbt-release" % "1.1.0") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") +addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.2.16") diff --git a/version.sbt b/version.sbt new file mode 100644 index 0000000..77a1b23 --- /dev/null +++ b/version.sbt @@ -0,0 +1 @@ +ThisBuild / version := "1.1.0-RC2"