Skip to content

Commit a371b14

Browse files
committed
Close #463: Move Eq and Show type class instances for refined4s.types.strings types from refined4s-cats to refined4s-core with orphan-cats
- Add `internalDef.contraCoercible` (which uses `cats.Contravariant` and `refined4s.Coercible`) to derive type class instances without exposing `cats` in `core`. - The following typeclass instances for strings are moved from `refined4s-cats` and rewritten with `orphan-cats`: - `NonEmptyString`: `derivedNonEmptyStringEq`, `derivedNonEmptyStringShow` - `NonBlankString`: `derivedNonBlankStringEq`, `derivedNonBlankStringShow` - `Uuid`: `derivedUuidEq`, `derivedUuidShow` - tests (`test-refined4s-core-without-cats`: JVM/JS/Native): - `internalDefSpec` validates `MissingCatsContravariant` message for `contraCoercible` - `stringsSpec` validates `MissingCatsEq` and `MissingCatsShow` messages for derived type class instances. No breaking changes. Compile-time tests cover absence-of-cats scenarios.
1 parent cc597c2 commit a371b14

File tree

12 files changed

+294
-24
lines changed

12 files changed

+294
-24
lines changed

build.sbt

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ lazy val refined4s = (project in file("."))
4242
coreJvm,
4343
coreJs,
4444
coreNative,
45+
testCoreWithoutCatsJvm,
46+
testCoreWithoutCatsJs,
47+
testCoreWithoutCatsNative,
4548
catsJvm,
4649
catsJs,
4750
catsNative,
@@ -72,7 +75,8 @@ lazy val core = module("core", crossProject(JVMPlatform, JSPlatform, Nativ
7275
.settings(
7376
scalacOptions ++= List("-Xprint-suspension"),
7477
libraryDependencies ++= List(
75-
libs.cats.value % Test,
78+
libs.orphanCats.value,
79+
libs.cats.value % Optional,
7680
libs.extrasCore.value % Test,
7781
),
7882
)
@@ -104,6 +108,19 @@ lazy val coreNative = core
104108
)
105109
)
106110

111+
lazy val testCoreWithoutCats = testModule("core-without-cats", crossProject(JVMPlatform, JSPlatform, NativePlatform))
112+
.settings(noPublish)
113+
.settings(
114+
scalacOptions ++= List("-Xprint-suspension"),
115+
libraryDependencies ++= List(
116+
libs.extrasCore.value % Test
117+
),
118+
)
119+
.dependsOn(core)
120+
lazy val testCoreWithoutCatsJvm = testCoreWithoutCats.jvm
121+
lazy val testCoreWithoutCatsJs = testCoreWithoutCats.js.settings(jsSettingsForFuture)
122+
lazy val testCoreWithoutCatsNative = testCoreWithoutCats.native.settings(nativeSettings)
123+
107124
lazy val cats = module("cats", crossProject(JVMPlatform, JSPlatform, NativePlatform))
108125
.settings(
109126
libraryDependencies ++= List(
@@ -152,6 +169,7 @@ lazy val pureconfig = module("pureconfig", crossProject(JVMPlatform))
152169
.settings(
153170
libraryDependencies ++= List(
154171
libs.pureconfigCore,
172+
libs.cats.value % Test,
155173
libs.extrasCore.value % Test,
156174
)
157175
)
@@ -195,7 +213,8 @@ lazy val doobieCe3Jvm = doobieCe3.jvm
195213
lazy val extrasRender = module("extras-render", crossProject(JVMPlatform, JSPlatform, NativePlatform))
196214
.settings(
197215
libraryDependencies ++= List(
198-
libs.extrasRender.value
216+
libs.extrasRender.value,
217+
libs.cats.value % Test,
199218
)
200219
)
201220
.dependsOn(
@@ -205,19 +224,20 @@ lazy val extrasRenderJvm = extrasRender.jvm
205224
lazy val extrasRenderJs = extrasRender.js.settings(jsSettingsForFuture)
206225
lazy val extrasRenderNative = extrasRender.native.settings(nativeSettings)
207226

208-
lazy val chimney = module("chimney", crossProject(JVMPlatform, JSPlatform, NativePlatform))
227+
lazy val chimney = module("chimney", crossProject(JVMPlatform, JSPlatform, NativePlatform))
209228
.settings(
210229
libraryDependencies ++= List(
211230
libs.chimney.value,
231+
libs.cats.value % Test,
212232
libs.tests.hedgehogExtraCore.value,
213233
libs.tests.hedgehogExtraRefined4s.value,
214234
)
215235
)
216236
.dependsOn(
217237
core % props.IncludeTest
218238
)
219-
lazy val chimneyJvm = chimney.jvm
220-
lazy val chimneyJs = chimney.js.settings(jsSettingsForFuture)
239+
lazy val chimneyJvm = chimney.jvm
240+
lazy val chimneyJs = chimney.js.settings(jsSettingsForFuture)
221241
lazy val chimneyNative = chimney.native.settings(nativeSettings)
222242

223243
lazy val refinedCompatScala2 = module("refined-compat-scala2", crossProject(JVMPlatform, JSPlatform, NativePlatform))
@@ -267,24 +287,30 @@ lazy val refinedCompatScala2Native = refinedCompatScala2
267287
)
268288

269289
lazy val refinedCompatScala3 = module("refined-compat-scala3", crossProject(JVMPlatform, JSPlatform, NativePlatform))
290+
.settings(
291+
libraryDependencies ++= List(
292+
libs.cats.value % Test
293+
)
294+
)
270295
.dependsOn(
271296
core % props.IncludeTest
272297
)
273298
lazy val refinedCompatScala3Jvm = refinedCompatScala3.jvm
274299
lazy val refinedCompatScala3Js = refinedCompatScala3.js.settings(jsSettingsForFuture)
275300
lazy val refinedCompatScala3Native = refinedCompatScala3.native.settings(nativeSettings)
276301

277-
lazy val tapir = module("tapir", crossProject(JVMPlatform, JSPlatform))//, NativePlatform))
302+
lazy val tapir = module("tapir", crossProject(JVMPlatform, JSPlatform)) // , NativePlatform))
278303
.settings(
279304
libraryDependencies ++= List(
280-
libs.tapirCore.value
305+
libs.tapirCore.value,
306+
libs.cats.value % Test,
281307
)
282308
)
283309
.dependsOn(
284310
core % props.IncludeTest
285311
)
286-
lazy val tapirJvm = tapir.jvm
287-
lazy val tapirJs = tapir.js.settings(jsSettingsForFuture)
312+
lazy val tapirJvm = tapir.jvm
313+
lazy val tapirJs = tapir.js.settings(jsSettingsForFuture)
288314
//lazy val tapirNative = tapir.native.settings(nativeSettings)
289315

290316
lazy val docs = (project in file("docs-gen-tmp/docs"))
@@ -413,6 +439,8 @@ lazy val props =
413439

414440
val LogbackVersion = "1.5.6"
415441

442+
val OrphanVersion = "0.5.0"
443+
416444
val KittensVersion = "3.0.0"
417445

418446
val TapirVersion = "1.11.28"
@@ -429,6 +457,8 @@ lazy val props =
429457

430458
lazy val libs = new {
431459

460+
lazy val orphanCats = Def.setting("io.kevinlee" %%% "orphan-cats" % props.OrphanVersion)
461+
432462
lazy val extrasCore = Def.setting("io.kevinlee" %%% "extras-core" % props.ExtrasVersion)
433463
lazy val extrasHedgehogCirce = Def.setting("io.kevinlee" %%% "extras-hedgehog-circe" % props.ExtrasVersion)
434464
lazy val extrasDoobieToolsCe2 = Def.setting("io.kevinlee" %%% "extras-doobie-tools-ce2" % props.ExtrasVersion)
@@ -495,6 +525,15 @@ def isScala3(scalaVersion: String): Boolean = scalaVersion.startsWith("3")
495525

496526
def module(projectName: String, crossProject: CrossProject.Builder): CrossProject = {
497527
val prefixedName = prefixedProjectName(projectName)
528+
commonModule(prefixedName, crossProject)
529+
}
530+
531+
def testModule(projectName: String, crossProject: CrossProject.Builder): CrossProject = {
532+
val prefixedName = s"test-${prefixedProjectName(projectName)}"
533+
commonModule(prefixedName, crossProject)
534+
}
535+
536+
def commonModule(prefixedName: String, crossProject: CrossProject.Builder): CrossProject = {
498537
crossProject
499538
.in(file(s"modules/$prefixedName"))
500539
.settings(

modules/refined4s-cats/shared/src/main/scala/refined4s/modules/cats/derivation/types/all.scala

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,6 @@ trait all {
107107

108108
// strings
109109

110-
inline given derivedNonEmptyStringEq(using eqActual: Eq[String]): Eq[NonEmptyString] = contraCoercible(eqActual)
111-
inline given derivedNonEmptyStringShow(using showActual: Show[String]): Show[NonEmptyString] = contraCoercible(showActual)
112-
113-
inline given derivedNonBlankStringEq(using eqActual: Eq[String]): Eq[NonBlankString] = contraCoercible(eqActual)
114-
inline given derivedNonBlankStringShow(using showActual: Show[String]): Show[NonBlankString] = contraCoercible(showActual)
115-
116-
inline given derivedUuidEq(using eqActual: Eq[String]): Eq[Uuid] = contraCoercible(eqActual)
117-
inline given derivedUuidShow(using showActual: Show[String]): Show[Uuid] = contraCoercible(showActual)
118-
119110
// network
120111

121112
inline given derivedUriEq(using eqActual: Eq[String]): Eq[Uri] = contraCoercible(eqActual)

modules/refined4s-circe/shared/src/test/scala/refined4s/modules/circe/derivation/types/CirceCodecWithTypeClassesForTypesSpec.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import hedgehog.runner.*
77
import io.circe.{Codec, Decoder, Encoder}
88
import refined4s.*
99
import refined4s.modules.cats.derivation.*
10-
import refined4s.modules.cats.derivation.types.all.given
1110
import refined4s.modules.circe.derivation.CirceNewtypeCodec
1211
import refined4s.modules.circe.derivation.types.all.given
1312
import refined4s.types.all.*
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package refined4s.types
2+
3+
import orphan.OrphanCats
4+
import refined4s.Coercible
5+
6+
/** @author Kevin Lee
7+
* @since 2025-08-21
8+
*/
9+
private[types] object internalDef extends OrphanCats {
10+
11+
inline def contraCoercible[F[*], A, B, G[*[*]]: CatsContravariant](
12+
inline fb: F[B]
13+
)(using inline contravariant: cats.Contravariant[F], coercible: Coercible[A, B]): F[A] =
14+
contravariant.contramap[B, A](fb)(coercible(_))
15+
16+
// inline def contraCoercible[F[*], A, B, G[*[*]]: CatsContravariant](
17+
// inline fb: F[B]
18+
// )(using inline catsContravariant: G[F], coercible: Coercible[A, B]): F[A] = {
19+
// @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
20+
// val contravariant = catsContravariant.asInstanceOf[cats.Contravariant[F]]
21+
// contravariant.contramap[B, A](fb)(coercible(_))
22+
// }
23+
24+
}

modules/refined4s-core/shared/src/main/scala/refined4s/types/strings.scala

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package refined4s.types
22

3+
import orphan.OrphanCats
4+
import orphan.OrphanCatsKernel
35
import refined4s.*
46

57
import java.util.UUID
@@ -26,7 +28,7 @@ trait strings {
2628
// scalafix:on
2729

2830
}
29-
object strings {
31+
object strings extends OrphanCats, OrphanCatsKernel {
3032

3133
type NonEmptyString = NonEmptyString.Type
3234
@SuppressWarnings(Array("org.wartremover.warts.Equals"))
@@ -41,6 +43,17 @@ object strings {
4143
@targetName("plus")
4244
def ++(thatNes: NonEmptyString): NonEmptyString = NonEmptyString.unsafeFrom(thisNes.value + thatNes.value)
4345
}
46+
47+
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
48+
given derivedNonEmptyStringEq[F[*]: CatsEq, G[*]: CatsEq](using eqActual: G[String]): F[NonEmptyString] = {
49+
internalDef.contraCoercible(eqActual.asInstanceOf[cats.Eq[String]])
50+
}.asInstanceOf[F[NonEmptyString]] // scalafix:ok DisableSyntax.asInstanceOf
51+
52+
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
53+
given derivedNonEmptyStringShow[F[*]: CatsShow, G[*]: CatsShow](using showActual: G[String]): F[NonEmptyString] = {
54+
internalDef.contraCoercible(showActual.asInstanceOf[cats.Show[String]])
55+
}.asInstanceOf[F[NonEmptyString]] // scalafix:ok DisableSyntax.asInstanceOf
56+
4457
}
4558

4659
val WhitespaceCharRange: List[(Int, Int)] =
@@ -87,6 +100,16 @@ object strings {
87100
inline def appendString(that: String): Type = NonBlankString.unsafeFrom(thisNbs.value + that)
88101
}
89102

103+
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
104+
inline given derivedNonBlankStringEq[F[*]: CatsEq, G[*]: CatsEq](using eqActual: G[String]): F[NonBlankString] = {
105+
internalDef.contraCoercible(eqActual.asInstanceOf[cats.Eq[String]])
106+
}.asInstanceOf[F[NonBlankString]] // scalafix:ok DisableSyntax.asInstanceOf
107+
108+
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
109+
inline given derivedNonBlankStringShow[F[*]: CatsShow, G[*]: CatsShow](using showActual: G[String]): F[NonBlankString] = {
110+
internalDef.contraCoercible(showActual.asInstanceOf[cats.Show[String]])
111+
}.asInstanceOf[F[NonBlankString]] // scalafix:ok DisableSyntax.asInstanceOf
112+
90113
}
91114

92115
type Uuid = Uuid.Type
@@ -113,6 +136,16 @@ object strings {
113136
def toUUID: UUID = UUID.fromString(uuid.value)
114137
}
115138

139+
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
140+
inline given derivedUuidEq[F[*]: CatsEq, G[*]: CatsEq](using eqActual: G[String]): F[Uuid] = {
141+
internalDef.contraCoercible(eqActual.asInstanceOf[cats.Eq[String]])
142+
}.asInstanceOf[F[Uuid]] // scalafix:ok DisableSyntax.asInstanceOf
143+
144+
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
145+
inline given derivedUuidShow[F[*]: CatsShow, G[*]: CatsShow](using showActual: G[String]): F[Uuid] = {
146+
internalDef.contraCoercible(showActual.asInstanceOf[cats.Show[String]])
147+
}.asInstanceOf[F[Uuid]] // scalafix:ok DisableSyntax.asInstanceOf
148+
116149
}
117150

118151
@SuppressWarnings(Array("org.wartremover.warts.Equals"))

modules/refined4s-doobie-ce2/shared/src/test/scala/refined4s/modules/doobie/derivation/Example.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import cats.*
44

55
import refined4s.*
66
import refined4s.modules.cats.derivation.*
7-
import refined4s.modules.cats.derivation.types.all.given
87
import refined4s.types.all.*
98

109
final case class Example(id: Example.Id, name: Example.Name, note: Example.Note, count: Example.Count)

modules/refined4s-doobie-ce2/shared/src/test/scala/refined4s/modules/doobie/derivation/ExampleWithDoobieGetPut.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package refined4s.modules.doobie.derivation
33
import cats.*
44
import refined4s.*
55
import refined4s.modules.cats.derivation.*
6-
import refined4s.modules.cats.derivation.types.all.given
76
import refined4s.modules.doobie.derivation.generic.auto.given
87
import refined4s.types.all.*
98

modules/refined4s-doobie-ce3/shared/src/test/scala/refined4s/modules/doobie/derivation/Example.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import cats.*
44

55
import refined4s.*
66
import refined4s.modules.cats.derivation.*
7-
import refined4s.modules.cats.derivation.types.all.given
87
import refined4s.types.all.*
98

109
final case class Example(id: Example.Id, name: Example.Name, note: Example.Note, count: Example.Count)

modules/refined4s-doobie-ce3/shared/src/test/scala/refined4s/modules/doobie/derivation/ExampleWithDoobieGetPut.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package refined4s.modules.doobie.derivation
33
import cats.*
44
import refined4s.*
55
import refined4s.modules.cats.derivation.*
6-
import refined4s.modules.cats.derivation.types.all.given
76
import refined4s.modules.doobie.derivation.generic.auto.given
87
import refined4s.types.all.*
98

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package refined4s
2+
3+
import orphan.OrphanCatsMessages
4+
5+
/** @author Kevin Lee
6+
* @since 2025-08-22
7+
*/
8+
object ExpectedErrorMessages {
9+
def missingEq: String = OrphanCatsMessages.MissingCatsEq
10+
11+
def missingShow: String = OrphanCatsMessages.MissingCatsShow
12+
13+
}

0 commit comments

Comments
 (0)