diff --git a/.scalafmt.conf b/.scalafmt.conf index 069ac9a8..c5ab00fe 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -19,3 +19,9 @@ rewriteTokens { "→" = "->" "←" = "<-" } + +project { + excludeFilters = [ + "/scala-3/" + ] +} \ No newline at end of file diff --git a/build.sbt b/build.sbt index 753668af..c6145eb3 100644 --- a/build.sbt +++ b/build.sbt @@ -107,6 +107,11 @@ lazy val `redis4cats-root` = project lazy val `redis4cats-core` = project .in(file("modules/core")) .settings(commonSettings: _*) + .settings(libraryDependencies += Libraries.literally) + .settings( + libraryDependencies ++= + pred(scalaVersion.value.startsWith("3"), t = Seq.empty, f = Seq(Libraries.reflect(scalaVersion.value))) + ) .settings(isMimaEnabled := true) .settings(Test / parallelExecution := false) .enablePlugins(AutomateHeaderPlugin) diff --git a/modules/core/src/main/scala-2/dev/profunktor/redis4cats/syntax/RedisURIOps.scala b/modules/core/src/main/scala-2/dev/profunktor/redis4cats/syntax/RedisURIOps.scala new file mode 100644 index 00000000..ff035086 --- /dev/null +++ b/modules/core/src/main/scala-2/dev/profunktor/redis4cats/syntax/RedisURIOps.scala @@ -0,0 +1,32 @@ +/* + * Copyright 2018-2021 ProfunKtor + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.profunktor.redis4cats.syntax + +import dev.profunktor.redis4cats.connection.RedisURI + +class RedisURIOps(val sc: StringContext) extends AnyVal { + def redis(args: Any*): RedisURI = macro macros.RedisLiteral.make +} + +trait RedisSyntax { + implicit def toRedisURIOps(sc: StringContext): RedisURIOps = + new RedisURIOps(sc) +} + +object literals extends RedisSyntax + + diff --git a/modules/core/src/main/scala-2/dev/profunktor/redis4cats/syntax/macros.scala b/modules/core/src/main/scala-2/dev/profunktor/redis4cats/syntax/macros.scala new file mode 100644 index 00000000..4c197c11 --- /dev/null +++ b/modules/core/src/main/scala-2/dev/profunktor/redis4cats/syntax/macros.scala @@ -0,0 +1,37 @@ +/* + * Copyright 2018-2021 ProfunKtor + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.profunktor.redis4cats.syntax + +import dev.profunktor.redis4cats.connection.RedisURI +import org.typelevel.literally.Literally + +object macros { + + object RedisLiteral extends Literally[RedisURI] { + + override def validate(c: Context)(s: String): Either[String, c.Expr[RedisURI]] = { + import c.universe._ + RedisURI.fromString(s) match { + case Left(e) => Left(e.getMessage) + case Right(_) => Right(c.Expr(q"dev.profunktor.redis4cats.connection.RedisURI.unsafeFromString($s)")) + } + } + + def make(c: Context)(args: c.Expr[Any]*): c.Expr[RedisURI] = apply(c)(args: _*) + } + +} diff --git a/modules/core/src/main/scala-3/dev.profunktor.redis4cats/TypeInqualityCompat.scala b/modules/core/src/main/scala-3/dev/profunktor/redis4cats/TypeInqualityCompat.scala similarity index 100% rename from modules/core/src/main/scala-3/dev.profunktor.redis4cats/TypeInqualityCompat.scala rename to modules/core/src/main/scala-3/dev/profunktor/redis4cats/TypeInqualityCompat.scala diff --git a/modules/core/src/main/scala-3/dev/profunktor/redis4cats/syntax.scala b/modules/core/src/main/scala-3/dev/profunktor/redis4cats/syntax.scala new file mode 100644 index 00000000..9feeed39 --- /dev/null +++ b/modules/core/src/main/scala-3/dev/profunktor/redis4cats/syntax.scala @@ -0,0 +1,35 @@ +/* + * Copyright 2018-2021 ProfunKtor + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.profunktor.redis4cats.syntax + +import dev.profunktor.redis4cats.connection.RedisURI +import org.typelevel.literally.Literally +import scala.language.`3.0` + +object literals { + extension (inline ctx: StringContext){ + inline def redis(inline args: Any*):RedisURI = ${RedisLiteral('ctx, 'args)} + } + + object RedisLiteral extends Literally[RedisURI]{ + def validate(s: String)(using Quotes) = + RedisURI.fromString(s) match { + case Left(e) => Left(e.getMessage) + case Right(_) => Right('{RedisURI.unsafeFromString(${ Expr(s) })}) + } + } +} \ No newline at end of file diff --git a/modules/core/src/main/scala/dev/profunktor/redis4cats/connection/RedisURI.scala b/modules/core/src/main/scala/dev/profunktor/redis4cats/connection/RedisURI.scala index 53bfae16..b70fb296 100644 --- a/modules/core/src/main/scala/dev/profunktor/redis4cats/connection/RedisURI.scala +++ b/modules/core/src/main/scala/dev/profunktor/redis4cats/connection/RedisURI.scala @@ -17,13 +17,26 @@ package dev.profunktor.redis4cats.connection import cats.ApplicativeThrow +import cats.implicits.toBifunctorOps import io.lettuce.core.{ RedisURI => JRedisURI } -sealed abstract case class RedisURI private (underlying: JRedisURI) +import scala.util.Try +import scala.util.control.NoStackTrace + +sealed abstract class RedisURI private (val underlying: JRedisURI) object RedisURI { def make[F[_]: ApplicativeThrow](uri: => String): F[RedisURI] = ApplicativeThrow[F].catchNonFatal(new RedisURI(JRedisURI.create(uri)) {}) def fromUnderlying(j: JRedisURI): RedisURI = new RedisURI(j) {} + + def fromString(uri: String): Either[InvalidRedisURI, RedisURI] = + Try(JRedisURI.create(uri)).toEither.bimap(InvalidRedisURI(uri, _), new RedisURI(_) {}) + + def unsafeFromString(uri: String): RedisURI = new RedisURI(JRedisURI.create(uri)) {} +} + +final case class InvalidRedisURI(uri: String, throwable: Throwable) extends NoStackTrace { + override def getMessage: String = Option(throwable.getMessage).getOrElse(s"Invalid Redis URI: $uri") } diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 69867dec..274c38a7 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -30,6 +30,10 @@ object Dependencies { val redisClient = "io.lettuce" % "lettuce-core" % V.lettuce + val literally = "org.typelevel" %% "literally" % "1.2.0" + + def reflect(version: String): ModuleID = "org.scala-lang" % "scala-reflect" % version + // Examples libraries val catsEffect = "org.typelevel" %% "cats-effect" % V.catsEffect val circeCore = "io.circe" %% "circe-core" % V.circe diff --git a/project/MimaVersionPlugin.scala b/project/MimaVersionPlugin.scala index a2246135..30d88a90 100644 --- a/project/MimaVersionPlugin.scala +++ b/project/MimaVersionPlugin.scala @@ -19,7 +19,7 @@ object MimaVersionPlugin extends AutoPlugin { override def trigger = allRequirements object autoImport { - val ReleaseTag = """^v((?:\d+\.){2}\d+(?:-.*)?)$""".r + val ReleaseTag = """^v((?:\d+\.){2}\d+(?:-.*)?)$""".r lazy val mimaBaseVersion = git.baseVersion lazy val mimaReportBinaryIssuesIfRelevant = taskKey[Unit]( "A wrapper around the mima task which ensures publishArtifact is set to true" @@ -45,44 +45,45 @@ object MimaVersionPlugin extends AutoPlugin { override def buildSettings: Seq[Setting[_]] = GitPlugin.autoImport.versionWithGit ++ Seq( - git.gitTagToVersionNumber := { - case ReleaseTag(version) => Some(version) - case _ => None - }, - git.formattedShaVersion := { - val suffix = git.makeUncommittedSignifierSuffix( - git.gitUncommittedChanges.value, - git.uncommittedSignifier.value + git.gitTagToVersionNumber := { + case ReleaseTag(version) => Some(version) + case _ => None + }, + git.formattedShaVersion := { + val suffix = git.makeUncommittedSignifierSuffix( + git.gitUncommittedChanges.value, + git.uncommittedSignifier.value + ) + + val description = Try("git describe --tags --match v*".!!.trim).toOption + val optDistance = description collect { + case Description(distance) => + distance + "-" + } + + val distance = optDistance.getOrElse("") + + git.gitHeadCommit.value map { _.substring(0, 7) } map { sha => + autoImport.mimaBaseVersion.value + "-" + distance + sha + suffix + } + }, + git.gitUncommittedChanges := Try("git status -s".!!.trim.length > 0) + .getOrElse(true), + git.gitHeadCommit := Try("git rev-parse HEAD".!!.trim).toOption, + git.gitCurrentTags := Try( + "git tag --contains HEAD".!!.trim.split("\\s+").toList.filter(_ != "") + ).toOption.toList.flatten ) - val description = Try("git describe --tags --match v*".!!.trim).toOption - val optDistance = description collect { case Description(distance) => - distance + "-" - } - - val distance = optDistance.getOrElse("") - - git.gitHeadCommit.value map { _.substring(0, 7) } map { sha => - autoImport.mimaBaseVersion.value + "-" + distance + sha + suffix - } - }, - git.gitUncommittedChanges := Try("git status -s".!!.trim.length > 0) - .getOrElse(true), - git.gitHeadCommit := Try("git rev-parse HEAD".!!.trim).toOption, - git.gitCurrentTags := Try( - "git tag --contains HEAD".!!.trim.split("\\s+").toList.filter(_ != "") - ).toOption.toList.flatten - ) - override def projectSettings: Seq[Setting[_]] = Seq( isMimaEnabled := false, mimaReportBinaryIssuesIfRelevant := filterTaskWhereRelevant( - mimaReportBinaryIssues - ).value, - mimaPreviousArtifacts := { + mimaReportBinaryIssues + ).value, + mimaPreviousArtifacts := { val current = version.value - val org = organization.value - val n = moduleName.value + val org = organization.value + val n = moduleName.value val FullTag = """^(\d+)\.(\d+)\.(\d+).*""" r val TagBase = """^(\d+)\.(\d+).*""" r @@ -100,8 +101,6 @@ object MimaVersionPlugin extends AutoPlugin { val tags = scala.util .Try("git tag --list".!!.split("\n").map(_.trim)) .getOrElse(new Array[String](0)) - println(tags.mkString("\n")) - // in semver, we allow breakage in minor releases if major is 0, otherwise not val Pattern = if (isPre) @@ -109,9 +108,10 @@ object MimaVersionPlugin extends AutoPlugin { else s"^v($major\\.\\d+\\.\\d+)$$".r - val versions = tags collect { case Pattern(version) => - version - } + val versions = tags collect { + case Pattern(version) => + version + } def lessThanPatch(patch: String): String => Boolean = { tagVersion => val FullTag(_, _, tagPatch) = tagVersion @@ -123,17 +123,15 @@ object MimaVersionPlugin extends AutoPlugin { .filterNot { val patchPredicate = maybePatch - // if mimaBaseVersion has a patch version, exclude this version if the patch is smaller + // if mimaBaseVersion has a patch version, exclude this version if the patch is smaller .map(lessThanPatch(_)) // else keep the version - .getOrElse { (_: String) => false } + .getOrElse((_: String) => false) v => patchPredicate(v) } notCurrent - .map(v => - projectID.value.withRevision(v).withExplicitArtifacts(Vector.empty) - ) + .map(v => projectID.value.withRevision(v).withExplicitArtifacts(Vector.empty)) .toSet } } diff --git a/site/docs/client.md b/site/docs/client.md index 4c083a01..65bc2f99 100644 --- a/site/docs/client.md +++ b/site/docs/client.md @@ -85,6 +85,28 @@ val configuredApi: Resource[IO, StringCommands[IO, String, String]] = } yield redis ``` +A RedisURI can also be created using the `redis` string interpolator: + +```scala mdoc:silent +import dev.profunktor.redis4cats.syntax.literals._ +val uri = redis"redis://localhost" + +val secure = redis"rediss://localhost" + +val withPassword = redis"redis://:password@localhost" + +val withDatabase = redis"redis://localhost/1" + +val sentinel = redis"redis-sentinel://localhost:26379,localhost:26380?sentinelMasterId=m" + +val `redis+ssl` = redis"redis+ssl://localhost" + +val `redis+tls` = redis"redis+tls://localhost" + +val `redis-socket` = redis"redis-socket:///tmp/redis.sock" + +``` + ## Single node connection For those who only need a simple API access to Redis commands, there are a few ways to acquire a connection: