├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .scalafmt.conf ├── LICENSE ├── README.md ├── build.sbt ├── circe-integration └── src │ ├── main │ └── scala │ │ └── pl │ │ └── msitko │ │ └── refined │ │ └── circe │ │ └── Codecs.scala │ └── test │ └── scala │ └── pl │ └── msitko │ └── refined │ └── circe │ └── CodecsSpec.scala ├── core └── src │ ├── main │ └── scala │ │ └── pl │ │ └── msitko │ │ └── refined │ │ ├── Refined.scala │ │ ├── compiletime │ │ ├── ValidateExprInt.scala │ │ ├── ValidateExprList.scala │ │ ├── ValidateExprString.scala │ │ ├── ValidateInt.scala │ │ ├── ValidateList.scala │ │ └── ValidateString.scala │ │ ├── macros │ │ └── ListMacros.scala │ │ └── runtime │ │ ├── ValidateExprInt.scala │ │ ├── ValidateExprList.scala │ │ └── ValidateExprString.scala │ └── test │ └── scala │ ├── example │ └── RefinedAccessSpec.scala │ └── pl │ └── msitko │ └── refined │ ├── IntSpec.scala │ ├── ListSpec.scala │ ├── RuntimeIntSpec.scala │ ├── StringSpec.scala │ ├── macros │ └── ListMacrosSpec.scala │ └── testUtils │ └── CompileTimeSuite.scala └── project ├── Common.scala ├── Dependencies.scala ├── build.properties └── plugins.sbt /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | pull_request: 5 | jobs: 6 | build: 7 | runs-on: ubuntu-20.04 8 | steps: 9 | - uses: actions/checkout@v3.3.0 10 | - uses: coursier/setup-action@v1 11 | with: 12 | jvm: adoptium:1.17 13 | apps: sbtn 14 | - run: sbtn test -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: ["*"] 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-20.04 8 | steps: 9 | - uses: actions/checkout@v3.3.0 10 | with: 11 | fetch-depth: 0 12 | - uses: coursier/setup-action@v1 13 | with: 14 | jvm: adoptium:1.17 15 | apps: sbtn 16 | - run: sbt ci-release 17 | env: 18 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 19 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 20 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 21 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .idea 3 | .DS_Store 4 | *.bloop 5 | *.metals 6 | .bsp 7 | .vscode 8 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.0.8 2 | runner.dialect = scala3 3 | 4 | align.preset = more 5 | assumeStandardLibraryStripMargin = true 6 | binPack.parentConstructors = true 7 | danglingParentheses.preset = false 8 | indentOperator.preset = spray 9 | maxColumn = 120 10 | newlines.topLevelStatementBlankLines = [ 11 | { blanks { before = 0, after = 0, beforeEndMarker = 0 } } 12 | ] 13 | rewrite.redundantBraces.maxLines = 5 14 | rewrite.rules = [RedundantBraces, RedundantParens, SortImports, SortModifiers, PreferCurlyFors] 15 | runner.optimizer.forceConfigStyleOnOffset = -1 16 | trailingCommas = preserve 17 | verticalMultiline.arityThreshold = 120 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Michal Sitko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mini-refined 2 | 3 | [![example workflow](https://github.com/note/mini-refined/actions/workflows/ci.yml/badge.svg)](https://github.com/note/mini-refined/actions) 4 | 5 | A proof of concept of a simple encoding of refinement types in Scala 3. 6 | 7 | You can read about motivation behind and the main concepts in the [blog post](https://msitko.pl/blog/build-your-own-refinement-types-in-scala3.html). 8 | 9 | ## Quick start 10 | 11 | Include library in `build.sbt`: 12 | 13 | ``` 14 | libraryDependencies += "pl.msitko" %% "mini-refined" % "0.2.0" 15 | ``` 16 | 17 | Common imports: 18 | 19 | ```scala 20 | import pl.msitko.refined.auto._ 21 | import pl.msitko.refined.Refined 22 | ``` 23 | 24 | ### Circe integration 25 | 26 | To use circe integration: 27 | 28 | ``` 29 | libraryDependencies += "pl.msitko" %% "mini-refined-circe" % "0.2.0" 30 | ``` 31 | 32 | ## Int predicates 33 | 34 | ```scala 35 | val a: Int Refined GreaterThan[10] = 5 36 | // fails compilation with: Validation failed: 5 > 10 37 | ``` 38 | 39 | ```scala 40 | val a: Int Refined LowerThan[10] = 15 41 | // fails compilation with: Validation failed: 15 < 10 42 | ``` 43 | 44 | ## String predicates 45 | 46 | ```scala 47 | val s: String Refined StartsWith["xyz"] = "abc" 48 | // fails compilation with: Validation failed: abc.startsWith(xyz) 49 | ``` 50 | 51 | ```scala 52 | val s: String Refined EndsWith["xyz"] = "abc" 53 | // fails compilation with: Validation failed: abc.endsWith(xyz) 54 | ``` 55 | 56 | ## List predicates 57 | 58 | ```scala 59 | val as: List[String] Refined Size[GreaterThan[1]] = List("a") 60 | // fails compilation with: 61 | // Validation failed: list size doesn't hold predicate: 1 > 1 62 | ``` 63 | 64 | You can use any `Int` predicates within `Size` predicate. 65 | 66 | ## Compose predicates with boolean operators 67 | 68 | You can compose predicates with boolean operators. For example: 69 | 70 | ```scala 71 | val c: Int Refined And[GreaterThan[10], LowerThan[20]] = 25 72 | // fails compilation with: Validation failed: (25 > 10 And 25 < 20), predicate failed: 25 < 20 73 | ``` 74 | 75 | ## Runtime validation 76 | 77 | Everything described so far works only for values known at a compile-time. However, values for most variables are coming 78 | at runtime. For those you need to use `Refined.refineV[T]` which returns `Either[String, T]`. Example: 79 | 80 | ```scala 81 | case class Example(a: Int, b: Int Refined GreaterThan[10]) 82 | 83 | def runtime(a: Int, b: Int): Either[String, Example] = 84 | Refined.refineV[GreaterThan[10]](b).map(refined => Example(a, refined)) 85 | ``` 86 | 87 | ## Inferring types compatibility 88 | 89 | `mini-refined` has some basic rules that enable using more specific types in places where more general types are required. 90 | 91 | In other words, considering such function: 92 | 93 | ```scala 94 | def intFun10(a: Int Refined GreaterThan[10]): Unit = ??? 95 | ``` 96 | 97 | We can call it with a value of type `Int Refined GreaterThan[20]`, as `mini-refined` recognizes that being greater than 20 implies being greater than 10. 98 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import Common._ 2 | 3 | lazy val miniRefined = (project in file("core")) 4 | .commonSettings("mini-refined") 5 | .settings( 6 | libraryDependencies ++= Dependencies.testDeps 7 | ) 8 | 9 | lazy val circeIntegration = (project in file("circe-integration")) 10 | .commonSettings("mini-refined-circe") 11 | .settings( 12 | libraryDependencies ++= Dependencies.circe ++ Dependencies.testDeps 13 | ) 14 | .dependsOn(miniRefined) 15 | 16 | lazy val rootProject = (project in file(".")) 17 | .commonSettings("root") 18 | .settings( 19 | // do not publish the root project 20 | publish / skip := true 21 | ) 22 | .aggregate(miniRefined, circeIntegration) 23 | -------------------------------------------------------------------------------- /circe-integration/src/main/scala/pl/msitko/refined/circe/Codecs.scala: -------------------------------------------------------------------------------- 1 | package pl.msitko.refined.circe 2 | 3 | import io.circe.{Decoder, Encoder} 4 | import pl.msitko.refined.Refined 5 | 6 | // TODO: Find a way to encode those codecs generically instead of defining them for each supported type 7 | // Things like the following are not getting picked up by implicits mechanism, i.e. they work only when given is called explicitly: 8 | // given [T : Decoder, P <: Refined.ValidateExprFor[T]]: Encoder[T Refined P] = ??? 9 | 10 | given intEncoder[P <: Refined.ValidateExprFor[Int]](using Encoder[Int]): Encoder[Int Refined P] = 11 | summon[Encoder[Int]].contramap(_.value) 12 | 13 | inline given intDecoder[P <: Refined.ValidateExprFor[Int]](using Decoder[Int]): Decoder[Int Refined P] = 14 | summon[Decoder[Int]].emap(v => Refined.refineV[P](v)) 15 | 16 | given stringEncoder[P <: Refined.ValidateExprFor[String]](using Encoder[String]): Encoder[String Refined P] = 17 | summon[Encoder[String]].contramap(_.value) 18 | 19 | inline given stringDecoder[P <: Refined.ValidateExprFor[String]](using Decoder[String]): Decoder[String Refined P] = 20 | summon[Decoder[String]].emap(v => Refined.refineV[P](v)) 21 | 22 | given listEncoder[T, P <: Refined.ValidateExprFor[List[Any]]](using Encoder[List[T]]): Encoder[List[T] Refined P] = 23 | summon[Encoder[List[T]]].contramap(_.value) 24 | 25 | inline given listDecoder[T, P <: Refined.ValidateExprFor[List[Any]]](using 26 | Decoder[List[T]]): Decoder[List[T] Refined P] = 27 | summon[Decoder[List[T]]].emap(v => Refined.refineV[T, P](v)) 28 | -------------------------------------------------------------------------------- /circe-integration/src/test/scala/pl/msitko/refined/circe/CodecsSpec.scala: -------------------------------------------------------------------------------- 1 | package pl.msitko.refined.circe 2 | 3 | import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} 4 | import io.circe.syntax.* 5 | import io.circe.{parser, Decoder, Encoder, Printer} 6 | import pl.msitko.refined.auto.* 7 | import pl.msitko.refined.compiletime.ValidateExprInt 8 | import pl.msitko.refined.compiletime.ValidateExprInt.GreaterThan 9 | import pl.msitko.refined.Refined 10 | 11 | final case class Library( 12 | name: String Refined StartsWith["lib"], 13 | version: Int Refined GreaterThan[10], 14 | dependencies: List[String] Refined Size[GreaterThan[1]]) 15 | 16 | class CodecsSpec extends munit.FunSuite: 17 | given enc: Encoder[Library] = deriveEncoder[Library] 18 | given dec: Decoder[Library] = deriveDecoder[Library] 19 | 20 | test("should roundtrip for a manually defined Encoder and Decoder for type Library") { 21 | val in = Library("libA", 23, List("depA", "depB")) 22 | val encoded = in.asJson.printWith(Printer.spaces2) 23 | val Right(decoded) = parser.parse(encoded).flatMap(_.as[Library]): @unchecked 24 | 25 | assertEquals(decoded, in) 26 | } 27 | 28 | test("decoder should fail for incorrect Library.name") { 29 | val in = """{ 30 | | "name" : "something", 31 | | "version" : 11, 32 | | "dependencies": ["depA", "depB"] 33 | |}""".stripMargin 34 | 35 | val Left(decodingError) = parser.parse(in).flatMap(_.as[Library]): @unchecked 36 | assertEquals( 37 | decodingError.getMessage, 38 | "DecodingFailure at .name: Validation of refined type failed: something.startWith(lib)") 39 | } 40 | 41 | test("decoder should fail for incorrect Library.version") { 42 | val in = """{ 43 | | "name" : "libA", 44 | | "version" : 7, 45 | | "dependencies": ["depA", "depB"] 46 | |}""".stripMargin 47 | 48 | val Left(decodingError) = parser.parse(in).flatMap(_.as[Library]): @unchecked 49 | assertEquals(decodingError.getMessage, "DecodingFailure at .version: Validation of refined type failed: 7 > 10") 50 | } 51 | 52 | test("decoder should fail for incorrect Library.dependencies") { 53 | val in = """{ 54 | | "name" : "libA", 55 | | "version" : 11, 56 | | "dependencies": ["depA"] 57 | |}""".stripMargin 58 | 59 | val Left(decodingError) = parser.parse(in).flatMap(_.as[Library]): @unchecked 60 | assertEquals( 61 | decodingError.getMessage, 62 | "DecodingFailure at .dependencies: Validation of refined type failed: list size doesn't hold predicate: 1 > 1") 63 | } 64 | -------------------------------------------------------------------------------- /core/src/main/scala/pl/msitko/refined/Refined.scala: -------------------------------------------------------------------------------- 1 | package pl.msitko.refined 2 | 3 | import scala.compiletime.{codeOf, constValue, erasedValue, error} 4 | import pl.msitko.refined.compiletime._ 5 | import pl.msitko.refined.compiletime.ValidateExprInt._ 6 | import pl.msitko.refined.compiletime.ValidateExprList.{And, Or} 7 | import quoted._ 8 | import pl.msitko.refined.runtime as RT 9 | 10 | import quoted.Expr 11 | 12 | object auto: 13 | 14 | export compiletime.ValidateExprInt.{And as _, Or as _, *} 15 | export compiletime.ValidateExprString.{And as _, Or as _, *} 16 | export compiletime.ValidateExprList.{And as _, Or as _, *} 17 | 18 | type ValidateExpr = compiletime.ValidateExprInt | compiletime.ValidateExprString | compiletime.ValidateExprList 19 | 20 | type And[A <: ValidateExpr, B <: ValidateExpr] = (A, B) match 21 | case (compiletime.ValidateExprInt, compiletime.ValidateExprInt) => compiletime.ValidateExprInt.And[A, B] 22 | case (compiletime.ValidateExprString, compiletime.ValidateExprString) => compiletime.ValidateExprString.And[A, B] 23 | case (compiletime.ValidateExprList, compiletime.ValidateExprList) => compiletime.ValidateExprList.And[A, B] 24 | 25 | type Or[A <: ValidateExpr, B <: ValidateExpr] = (A, B) match 26 | case (compiletime.ValidateExprInt, compiletime.ValidateExprInt) => compiletime.ValidateExprInt.Or[A, B] 27 | case (compiletime.ValidateExprString, compiletime.ValidateExprString) => compiletime.ValidateExprString.Or[A, B] 28 | case (compiletime.ValidateExprList, compiletime.ValidateExprList) => compiletime.ValidateExprList.Or[A, B] 29 | 30 | implicit inline def mkValidatedInt[V <: Int & Singleton, E <: ValidateExprInt](v: V): Refined[V, E] = 31 | inline ValidateInt.validate[V, E] match 32 | case null => Refined.unsafeApply(v) 33 | case failMsg => 34 | inline val wholePredicateMsg = ValidateInt.showPredicate[V, E] 35 | inline if wholePredicateMsg == failMsg then reportError("Validation failed: " + wholePredicateMsg) 36 | else reportError("Validation failed: " + wholePredicateMsg + ", predicate failed: " + failMsg) 37 | 38 | implicit inline def mkValidatedString[V <: String & Singleton, E <: ValidateExprString](v: V): Refined[V, E] = 39 | inline ValidateString.validate[V, E] match 40 | case null => Refined.unsafeApply(v) 41 | case failMsg => 42 | inline val wholePredicateMsg = ValidateString.showPredicate[V, E] 43 | inline if wholePredicateMsg == ValidateString.validate[V, E] then 44 | reportError("Validation failed: " + wholePredicateMsg) 45 | else 46 | reportError( 47 | "Validation failed: " + wholePredicateMsg + ", predicate failed: " + ValidateString.validate[V, E]) 48 | 49 | implicit inline def mkValidatedList[T, E <: ValidateExprList](inline v: List[T]): Refined[List[T], E] = 50 | inline ValidateList.validate[E](v) match 51 | case null => Refined.unsafeApply(v) 52 | case failMsg => reportError("Validation failed: " + failMsg) 53 | 54 | implicit inline def intLowerThanInference[T <: Int & Singleton, U <: Int & Singleton]( 55 | v: Int Refined LowerThan[T]): Int Refined LowerThan[U] = 56 | inline erasedValue[T] < erasedValue[U] match 57 | case _: true => Refined.unsafeApply(v.value) 58 | case _: false => reportError("Cannot be inferred") 59 | 60 | implicit inline def intGreaterThanInference[T <: Int & Singleton, U <: Int & Singleton]( 61 | v: Int Refined GreaterThan[T]): Int Refined GreaterThan[U] = 62 | inline erasedValue[T] > erasedValue[U] match 63 | case _: true => Refined.unsafeApply(v.value) 64 | case _: false => reportError("Cannot be inferred") 65 | 66 | // hack around scala.compiletime.error limitation that its argument has to be a literal 67 | // See: https://github.com/lampepfl/dotty/issues/10315 68 | private inline def reportError(inline a: String): Nothing = ${ reportErrorCode('a) } 69 | 70 | private def reportErrorCode(a: Expr[String])(using q: Quotes): Nothing = 71 | q.reflect.report.errorAndAbort(a.valueOrAbort) 72 | 73 | // private transparent inline def stringEquals(inline a: String, inline b: String): Boolean = ${ stringEqualsCode('a, 'b) } 74 | // 75 | // private def stringEqualsCode(a: Expr[String], b: Expr[String])(using q: Quotes): Expr[Boolean] = 76 | // import quotes.reflect.* 77 | // println("bazinga 002") 78 | // println(a.asTerm.show(using Printer.TreeStructure)) 79 | // println(b.asTerm.show(using Printer.TreeStructure)) 80 | // println(Expr.betaReduce(b).show) 81 | // val res = a.valueOrError == Expr.betaReduce(b).valueOrError 82 | // Expr(res) 83 | 84 | //opaque type Refined[Underlying, ValidateExpr] = Underlying 85 | // I couldn't make the following work with opaque type: 86 | // val a: Refined[Int, GreaterThan[10]] = 186 87 | // That worked well with opaque type: 88 | // val a: Int Refined GreaterThan[10] = mkValidatedInt[16, GreaterThan[10]](16) 89 | 90 | // T is covariant so `val a: Refined[Int, GreaterThan[10]] = 186` works as well as `val a: Refined[186, GreaterThan[10]] = 186` 91 | // but not sure how important it's that the latter works 92 | final class Refined[+T, P <: Refined.ValidateExprFor[T]] private (val value: T) extends AnyVal { 93 | override def toString: String = value.toString 94 | } 95 | 96 | //trait Refined[+Underlying, ValidateExpr] 97 | 98 | object Refined: 99 | // type Base = Int | String | List[Any] 100 | 101 | type ValidateExprFor[B] = B match 102 | case Int => ValidateExprInt 103 | case String => ValidateExprString 104 | case List[Any] => ValidateExprList 105 | 106 | // We cannot simply `implicit inline def mk...(): Refined` because inline and opaque types do not compose 107 | // Read about it here: https://github.com/lampepfl/dotty/issues/6802 108 | private[refined] def unsafeApply[T <: Int, P <: ValidateExprInt](i: T): T Refined P = new Refined[T, P](i) 109 | 110 | private[refined] def unsafeApply[T <: String, P <: ValidateExprString](i: T): T Refined P = 111 | new Refined[T, P](i) 112 | 113 | private[refined] def unsafeApply[T, P <: ValidateExprList](i: List[T]): List[T] Refined P = new Refined[List[T], P](i) 114 | implicit def unwrap[T <: Int, P <: ValidateExprInt](in: Refined[T, P]): T = in.value 115 | implicit def unwrap[T <: String, P <: ValidateExprString](in: Refined[T, P]): T = in.value 116 | implicit def unwrap[X, T <: List[X], P <: ValidateExprList](in: Refined[T, P]): List[X] = in.value 117 | 118 | inline def refineV[P <: ValidateExprInt](v: Int): Either[String, Int Refined P] = 119 | RT.ValidateExprInt.fromCompiletime[P].validate(v) match 120 | case Some(err) => Left(s"Validation of refined type failed: $err") 121 | case None => Right(Refined.unsafeApply[Int, P](v)) 122 | 123 | inline def refineV[P <: ValidateExprString](v: String): Either[String, String Refined P] = 124 | RT.ValidateExprString.fromCompiletime[P].validate(v) match 125 | case Some(err) => Left(s"Validation of refined type failed: $err") 126 | case None => Right(Refined.unsafeApply[String, P](v)) 127 | 128 | inline def refineV[T, P <: ValidateExprList](v: List[T]): Either[String, List[T] Refined P] = 129 | RT.ValidateExprList.fromCompiletime[P].validate(v) match 130 | case Some(err) => Left(s"Validation of refined type failed: $err") 131 | case None => Right(Refined.unsafeApply[T, P](v)) 132 | -------------------------------------------------------------------------------- /core/src/main/scala/pl/msitko/refined/compiletime/ValidateExprInt.scala: -------------------------------------------------------------------------------- 1 | package pl.msitko.refined.compiletime 2 | 3 | sealed trait ValidateExprInt 4 | 5 | object ValidateExprInt: 6 | class And[A <: ValidateExprInt, B <: ValidateExprInt] extends ValidateExprInt 7 | class Or[A <: ValidateExprInt, B <: ValidateExprInt] extends ValidateExprInt 8 | class LowerThan[T <: Int & Singleton] extends ValidateExprInt 9 | class GreaterThan[T <: Int & Singleton] extends ValidateExprInt 10 | -------------------------------------------------------------------------------- /core/src/main/scala/pl/msitko/refined/compiletime/ValidateExprList.scala: -------------------------------------------------------------------------------- 1 | package pl.msitko.refined.compiletime 2 | 3 | sealed trait ValidateExprList 4 | 5 | object ValidateExprList: 6 | class And[A <: ValidateExprList, B <: ValidateExprList] extends ValidateExprList 7 | class Or[A <: ValidateExprList, B <: ValidateExprList] extends ValidateExprList 8 | class Size[T <: ValidateExprInt] extends ValidateExprList 9 | -------------------------------------------------------------------------------- /core/src/main/scala/pl/msitko/refined/compiletime/ValidateExprString.scala: -------------------------------------------------------------------------------- 1 | package pl.msitko.refined.compiletime 2 | 3 | sealed trait ValidateExprString 4 | 5 | object ValidateExprString: 6 | class And[A <: ValidateExprString, B <: ValidateExprString] extends ValidateExprString 7 | class Or[A <: ValidateExprString, B <: ValidateExprString] extends ValidateExprString 8 | class StartsWith[T <: String & Singleton] extends ValidateExprString 9 | class EndsWith[T <: String & Singleton] extends ValidateExprString 10 | -------------------------------------------------------------------------------- /core/src/main/scala/pl/msitko/refined/compiletime/ValidateInt.scala: -------------------------------------------------------------------------------- 1 | package pl.msitko.refined.compiletime 2 | 3 | import scala.compiletime.ops.any.ToString 4 | import scala.compiletime.{codeOf, constValue, erasedValue, error} 5 | import pl.msitko.refined.compiletime.ValidateExprInt.{And, GreaterThan, LowerThan, Or} 6 | 7 | object ValidateInt: 8 | 9 | transparent inline def validate[V <: Int & Singleton, E <: ValidateExprInt]: String | Null = 10 | validateV[E](constValue[V], constValue[ToString[V]]) 11 | 12 | // It used to have Option[String] as a return type but there were some glitches when trying to report errors: 13 | // Expected a known value. 14 | // [error] 15 | // [error] The value of: "Validation failedd: (25 > 10 And 25 < 20)".+(failMsg) 16 | // [error] could not be extracted using scala.quoted.FromExpr$PrimitiveFromExpr@60d33381 17 | transparent inline def validateV[E <: ValidateExprInt](v: Int, asString: String): String | Null = 18 | inline erasedValue[E] match 19 | case _: LowerThan[t] => 20 | inline v < constValue[t] match 21 | case _: true => null 22 | case _: false => showPredicateV[E](asString) 23 | case _: GreaterThan[t] => 24 | inline v > constValue[t] match 25 | case _: true => null 26 | case _: false => showPredicateV[E](asString) 27 | case _: And[a, b] => 28 | inline validateV[a](v, asString) match 29 | case null => 30 | validateV[b](v, asString) 31 | case _ => 32 | validateV[a](v, asString) 33 | case _: Or[a, b] => 34 | inline validateV[a](v, asString) match 35 | case null => null 36 | case msg => 37 | inline validateV[b](v, asString) match 38 | case null => null 39 | case msg => 40 | showPredicateV[E](asString) 41 | 42 | transparent inline def showPredicate[V <: Int & Singleton, E <: ValidateExprInt]: String = 43 | showPredicateV[E](constValue[ToString[V]]) 44 | 45 | transparent inline def showPredicateV[E <: ValidateExprInt](asString: String): String = 46 | inline erasedValue[E] match 47 | case _: LowerThan[t] => 48 | asString + " < " + constValue[ToString[t]] 49 | case _: GreaterThan[t] => 50 | asString + " > " + constValue[ToString[t]] 51 | case _: And[a, b] => 52 | inline val aMsg = showPredicateV[a](asString) 53 | inline val bMsg = showPredicateV[b](asString) 54 | "(" + aMsg + " And " + bMsg + ")" 55 | case _: Or[a, b] => 56 | inline val aMsg = showPredicateV[a](asString) 57 | inline val bMsg = showPredicateV[b](asString) 58 | "(" + aMsg + " Or " + bMsg + ")" 59 | -------------------------------------------------------------------------------- /core/src/main/scala/pl/msitko/refined/compiletime/ValidateList.scala: -------------------------------------------------------------------------------- 1 | package pl.msitko.refined.compiletime 2 | 3 | import pl.msitko.refined.compiletime.ValidateExprList._ 4 | import pl.msitko.refined.macros.ListMacros 5 | 6 | import scala.compiletime.erasedValue 7 | 8 | object ValidateList: 9 | transparent inline def validate[E <: ValidateExprList](inline in: List[_]): String | Null = 10 | inline erasedValue[E] match 11 | case _: Size[t] => 12 | inline ValidateInt.validateV[t](ListMacros.listSize(in), ListMacros.listSizeString(in)) match 13 | case null => null 14 | case failMsg => "list size doesn't hold predicate: " + failMsg 15 | case _ => 16 | "Couldn't be validated as List" 17 | -------------------------------------------------------------------------------- /core/src/main/scala/pl/msitko/refined/compiletime/ValidateString.scala: -------------------------------------------------------------------------------- 1 | package pl.msitko.refined.compiletime 2 | 3 | import scala.compiletime.{codeOf, constValue, erasedValue, error} 4 | import pl.msitko.refined.compiletime.ValidateExprString._ 5 | 6 | import quoted.{Expr, Quotes} 7 | 8 | object ValidateString: 9 | 10 | transparent inline def validate[V <: String & Singleton, E <: ValidateExprString]: String | Null = 11 | inline erasedValue[E] match 12 | case _: StartsWith[t] => 13 | inline startsWith(constValue[V], constValue[t]) match 14 | case _: true => null 15 | case _: false => 16 | showPredicateV[E](constValue[V]) 17 | case _: EndsWith[t] => 18 | inline endsWith(constValue[V], constValue[t]) match 19 | case _: true => null 20 | case _: false => 21 | showPredicateV[E](constValue[V]) 22 | case _: And[a, b] => 23 | // workaround for: https://github.com/lampepfl/dotty/issues/12715 24 | inline val res = validate[V, a] 25 | inline res match 26 | case null => 27 | validate[V, b] 28 | case _ => res 29 | case _: Or[a, b] => 30 | inline validate[V, a] match 31 | case null => null 32 | case _ => validate[V, b] 33 | 34 | private transparent inline def startsWith(inline v: String, inline pred: String): Boolean = 35 | ${ startsWithCode('v, 'pred) } 36 | 37 | private def startsWithCode(v: Expr[String], pred: Expr[String])(using Quotes): Expr[Boolean] = 38 | val res = v.valueOrAbort.startsWith(pred.valueOrAbort) 39 | Expr(res) 40 | 41 | private transparent inline def endsWith(inline v: String, inline pred: String): Boolean = 42 | ${ endsWithCode('v, 'pred) } 43 | 44 | private def endsWithCode(v: Expr[String], pred: Expr[String])(using Quotes): Expr[Boolean] = 45 | val res = v.valueOrAbort.endsWith(pred.valueOrAbort) 46 | Expr(res) 47 | 48 | transparent inline def showPredicate[V <: String & Singleton, E <: ValidateExprString]: String = 49 | showPredicateV[E](constValue[V]) 50 | 51 | transparent inline def showPredicateV[E <: ValidateExprString](v: String): String = 52 | inline erasedValue[E] match 53 | case _: StartsWith[t] => 54 | v + ".startsWith(" + constValue[t] + ")" 55 | case _: EndsWith[t] => 56 | v + ".endsWith(" + constValue[t] + ")" 57 | case _: And[a, b] => 58 | inline val aMsg = showPredicateV[a](v) 59 | inline val bMsg = showPredicateV[b](v) 60 | "(" + aMsg + " And " + bMsg + ")" 61 | case _: Or[a, b] => 62 | inline val aMsg = showPredicateV[a](v) 63 | inline val bMsg = showPredicateV[b](v) 64 | "(" + aMsg + " Or " + bMsg + ")" 65 | -------------------------------------------------------------------------------- /core/src/main/scala/pl/msitko/refined/macros/ListMacros.scala: -------------------------------------------------------------------------------- 1 | package pl.msitko.refined.macros 2 | 3 | import quoted.* 4 | 5 | object ListMacros: 6 | 7 | transparent inline def listSize[T](inline in: List[T]): Int = 8 | ${ listSizeCode('in) } 9 | 10 | def listSizeCode[T](in: Expr[List[T]])(using q: Quotes): Expr[Int] = 11 | import quotes.reflect.* 12 | def rec(tree: Term): Expr[Int] = 13 | tree match 14 | case Inlined(_, _, i: Inlined) => 15 | rec(i) 16 | case Inlined(_, _, Apply(TypeApply(Select(Ident("List"), "apply"), _), List(Typed(Repeated(xs, _), _)))) => 17 | Expr(xs.size) 18 | case Inlined(_, _, TypeApply(Select(Ident("List"), "empty"), _)) => 19 | Expr(0) 20 | case Inlined(None, Nil, Ident("Nil")) => 21 | Expr(0) 22 | case _ => 23 | val treeStr = in.asTerm.show(using Printer.TreeStructure) 24 | q.reflect.report.errorAndAbort(s"Cannot determine size of list in compiletime. Tree: $treeStr") 25 | rec(in.asTerm) 26 | 27 | // TODO: Remove code duplication as it's basically the same as listSize 28 | transparent inline def listSizeString[T](inline in: List[T]): String = 29 | ${ listSizeStringCode('in) } 30 | 31 | def listSizeStringCode[T](in: Expr[List[T]])(using q: Quotes): Expr[String] = 32 | import quotes.reflect.* 33 | def rec(tree: Term): Expr[String] = 34 | tree match 35 | case Inlined(_, _, i: Inlined) => 36 | rec(i) 37 | case Inlined(_, _, Apply(TypeApply(Select(Ident("List"), "apply"), _), List(Typed(Repeated(xs, _), _)))) => 38 | Expr(xs.size.toString) 39 | case Inlined(_, _, TypeApply(Select(Ident("List"), "empty"), _)) => 40 | Expr("0") 41 | case Inlined(None, Nil, Ident("Nil")) => 42 | Expr("0") 43 | case _ => 44 | val treeStr = in.asTerm.show(using Printer.TreeStructure) 45 | q.reflect.report.errorAndAbort(s"Cannot determine size of list in compiletime. Tree: $treeStr") 46 | rec(in.asTerm) 47 | -------------------------------------------------------------------------------- /core/src/main/scala/pl/msitko/refined/runtime/ValidateExprInt.scala: -------------------------------------------------------------------------------- 1 | package pl.msitko.refined.runtime 2 | 3 | import compiletime.{constValue, erasedValue} 4 | import pl.msitko.refined.compiletime as CT 5 | import pl.msitko.refined.runtime.ValidateExprInt.{And, LowerThan} 6 | 7 | sealed trait ValidateExprInt { 8 | def validate(v: Int): Option[String] 9 | } 10 | 11 | object ValidateExprInt: 12 | 13 | final case class And(a: ValidateExprInt, b: ValidateExprInt) extends ValidateExprInt: 14 | def validate(v: Int): Option[String] = a.validate(v).orElse(b.validate(v)) 15 | 16 | final case class Or(a: ValidateExprInt, b: ValidateExprInt) extends ValidateExprInt: 17 | def validate(v: Int): Option[String] = a.validate(v) match 18 | case Some(err) => 19 | b.validate(v) match 20 | case Some(err2) => Some(s"($err Or $err2)") 21 | case None => None 22 | case None => 23 | None 24 | 25 | final case class LowerThan(t: Int) extends ValidateExprInt: 26 | def validate(v: Int): Option[String] = 27 | if v < t then None 28 | else Some(s"$v < $t") 29 | 30 | final case class GreaterThan(t: Int) extends ValidateExprInt: 31 | def validate(v: Int): Option[String] = 32 | if v > t then None 33 | else Some(s"$v > $t") 34 | 35 | inline def fromCompiletime[T <: CT.ValidateExprInt]: ValidateExprInt = 36 | inline erasedValue[T] match 37 | case _: CT.ValidateExprInt.And[a, b] => And(fromCompiletime[a], fromCompiletime[b]) 38 | case _: CT.ValidateExprInt.Or[a, b] => Or(fromCompiletime[a], fromCompiletime[b]) 39 | case _: CT.ValidateExprInt.LowerThan[t] => LowerThan(constValue[t]) 40 | case _: CT.ValidateExprInt.GreaterThan[t] => GreaterThan(constValue[t]) 41 | -------------------------------------------------------------------------------- /core/src/main/scala/pl/msitko/refined/runtime/ValidateExprList.scala: -------------------------------------------------------------------------------- 1 | package pl.msitko.refined.runtime 2 | 3 | import compiletime.{constValue, erasedValue} 4 | import pl.msitko.refined.compiletime as CT 5 | import pl.msitko.refined.runtime as RT 6 | 7 | sealed trait ValidateExprList: 8 | def validate(v: List[_]): Option[String] 9 | 10 | object ValidateExprList: 11 | final case class Size(sizeIntValidator: RT.ValidateExprInt) extends ValidateExprList: 12 | def validate(v: List[_]): Option[String] = 13 | sizeIntValidator.validate(v.size).map(err => s"list size doesn't hold predicate: $err") 14 | 15 | inline def fromCompiletime[T <: CT.ValidateExprList]: ValidateExprList = 16 | inline erasedValue[T] match 17 | case _: CT.ValidateExprList.Size[t] => Size(RT.ValidateExprInt.fromCompiletime[t]) 18 | -------------------------------------------------------------------------------- /core/src/main/scala/pl/msitko/refined/runtime/ValidateExprString.scala: -------------------------------------------------------------------------------- 1 | package pl.msitko.refined.runtime 2 | 3 | import compiletime.{constValue, erasedValue} 4 | import pl.msitko.refined.compiletime as CT 5 | 6 | sealed trait ValidateExprString: 7 | def validate(v: String): Option[String] 8 | 9 | object ValidateExprString: 10 | 11 | final case class And(a: ValidateExprString, b: ValidateExprString) extends ValidateExprString: 12 | def validate(v: String): Option[String] = a.validate(v).orElse(b.validate(v)) 13 | 14 | final case class Or(a: ValidateExprString, b: ValidateExprString) extends ValidateExprString: 15 | def validate(v: String): Option[String] = a.validate(v) match 16 | case Some(err) => 17 | b.validate(v) match 18 | case Some(err2) => Some(s"($err Or $err2)") 19 | case None => None 20 | case None => 21 | None 22 | 23 | final case class StartsWith(t: String) extends ValidateExprString: 24 | def validate(v: String): Option[String] = 25 | if v.startsWith(t) then None 26 | else Some(s"$v.startWith($t)") 27 | 28 | final case class EndsWith(t: String) extends ValidateExprString: 29 | def validate(v: String): Option[String] = 30 | if v.endsWith(t) then None 31 | else Some(s"$v.endsWith($t)") 32 | 33 | inline def fromCompiletime[T <: CT.ValidateExprString]: ValidateExprString = 34 | inline erasedValue[T] match 35 | case _: CT.ValidateExprString.And[a, b] => And(fromCompiletime[a], fromCompiletime[b]) 36 | case _: CT.ValidateExprString.Or[a, b] => Or(fromCompiletime[a], fromCompiletime[b]) 37 | case _: CT.ValidateExprString.StartsWith[t] => StartsWith(constValue[t]) 38 | case _: CT.ValidateExprString.EndsWith[t] => EndsWith(constValue[t]) 39 | -------------------------------------------------------------------------------- /core/src/test/scala/example/RefinedAccessSpec.scala: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import scala.compiletime.testing.{typeCheckErrors => errors} 4 | import pl.msitko.refined.testUtils.CompileTimeSuite 5 | 6 | // This test is out side of pl.msitko.refined so we can test some methods are not accessible 7 | class RefinedAccessSpec extends CompileTimeSuite { 8 | 9 | test("Refined.unsafeApply should not compile outside of pl.msitko.refined package") { 10 | val es = errors("pl.msitko.refined.Refined.unsafeApply[34, GreaterThan[10]](34)") 11 | assert( 12 | clue(es.head.message) 13 | .contains("none of the overloaded alternatives named unsafeApply can be accessed as a member")) 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /core/src/test/scala/pl/msitko/refined/IntSpec.scala: -------------------------------------------------------------------------------- 1 | package pl.msitko.refined 2 | 3 | import pl.msitko.refined.Refined._ 4 | import pl.msitko.refined.auto._ 5 | import pl.msitko.refined.testUtils.CompileTimeSuite 6 | 7 | import scala.annotation.nowarn 8 | import scala.compiletime.testing.{typeCheckErrors => errors} 9 | import scala.language.implicitConversions 10 | 11 | class IntSpec extends CompileTimeSuite { 12 | 13 | test("GreaterThan[10] should fail for lower or equal to to") { 14 | shouldContain(errors("mkValidatedInt[7, GreaterThan[10]](7)"), "Validation failed: 7 > 10") 15 | shouldContain(errors("mkValidatedInt[10, GreaterThan[10]](10)"), "Validation failed: 10 > 10") 16 | } 17 | 18 | test("GreaterThan[10] should fail for lower or equal to to (implicitly)") { 19 | shouldContain(errors("val a: Int Refined GreaterThan[10] = 7"), "Validation failed: 7 > 10") 20 | } 21 | 22 | test("GreaterThan[10] should pass for greater than 10") { 23 | val a: Int Refined GreaterThan[10] = mkValidatedInt[16, GreaterThan[10]](16) 24 | assertEquals(a + 0, 16) 25 | } 26 | 27 | test("GreaterThan[10] should pass for greater than 10 (implicitly)") { 28 | val a: Refined[Int, GreaterThan[10]] = 16 29 | assertEquals(a + 0, 16) 30 | } 31 | 32 | test("GreaterThan And LowerThan") { 33 | val a: Int Refined And[GreaterThan[10], LowerThan[20]] = 15 34 | assertEquals(a + 0, 15) 35 | } 36 | 37 | test("GreaterThan And LowerThan - failure") { 38 | shouldContain( 39 | errors("val a: Int Refined And[GreaterThan[10], LowerThan[20]] = 5"), 40 | "Validation failed: (5 > 10 And 5 < 20), predicate failed: 5 > 10") 41 | shouldContain( 42 | errors("val b: Int Refined And[GreaterThan[10], LowerThan[20]] = 25"), 43 | "Validation failed: (25 > 10 And 25 < 20), predicate failed: 25 < 20") 44 | } 45 | 46 | test("nested boolean conditions") { 47 | val a: Int Refined Or[And[GreaterThan[10], LowerThan[20]], And[GreaterThan[110], LowerThan[120]]] = 15 48 | val b: Int Refined Or[And[GreaterThan[10], LowerThan[20]], And[GreaterThan[110], LowerThan[120]]] = 115 49 | assertEquals(a + 0, 15) 50 | assertEquals(b + 0, 115) 51 | } 52 | 53 | test("nested boolean conditions - failure") { 54 | shouldContain( 55 | errors("val a: Int Refined Or[And[GreaterThan[10], LowerThan[20]], And[GreaterThan[110], LowerThan[120]]] = 5"), 56 | "Validation failed: ((5 > 10 And 5 < 20) Or (5 > 110 And 5 < 120))") 57 | shouldContain( 58 | errors("val a: Int Refined Or[And[GreaterThan[10], LowerThan[20]], And[GreaterThan[110], LowerThan[120]]] = 35"), 59 | "Validation failed: ((35 > 10 And 35 < 20) Or (35 > 110 And 35 < 120))") 60 | shouldContain( 61 | errors("val a: Int Refined Or[And[GreaterThan[10], LowerThan[20]], And[GreaterThan[110], LowerThan[120]]] = 125"), 62 | "Validation failed: ((125 > 10 And 125 < 20) Or (125 > 110 And 125 < 120))") 63 | } 64 | 65 | test("basic inference (GreaterThan)") { 66 | val a: Int Refined GreaterThan[10] = 16 67 | @nowarn("msg=unused local definition") 68 | val b: Int Refined GreaterThan[5] = a 69 | shouldContain(errors("val c: Int Refined GreaterThan[15] = a"), "Cannot be inferred") 70 | 71 | } 72 | 73 | test("basic inference (LowerThan)") { 74 | val a: Int Refined LowerThan[10] = 7 75 | @nowarn("msg=unused local definition") 76 | val b: Int Refined LowerThan[15] = a 77 | shouldContain(errors("val c: Int Refined LowerThan[5] = a"), "Cannot be inferred") 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /core/src/test/scala/pl/msitko/refined/ListSpec.scala: -------------------------------------------------------------------------------- 1 | package pl.msitko.refined 2 | 3 | import scala.annotation.nowarn 4 | // TODO: those imports are too complicated 5 | import pl.msitko.refined.compiletime.ValidateExprList._ 6 | import pl.msitko.refined.compiletime.ValidateExprInt._ 7 | import pl.msitko.refined.auto._ 8 | import pl.msitko.refined.testUtils.CompileTimeSuite 9 | 10 | import scala.compiletime.testing.{typeCheckErrors => errors} 11 | 12 | class ListSpec extends CompileTimeSuite { 13 | 14 | test("Size[GreaterThan] should pass") { 15 | @nowarn("msg=unused local definition") 16 | val a: List[String] Refined Size[GreaterThan[1]] = List("a", "b") 17 | @nowarn("msg=unused local definition") 18 | val b: List[String] Refined Size[GreaterThan[1]] = List("a", "b", "c") 19 | } 20 | 21 | test("Size[GreaterThan] should fail for incorrect value") { 22 | shouldContain( 23 | errors("val a: List[String] Refined Size[GreaterThan[1]] = List.empty"), 24 | "Validation failed: list size doesn't hold predicate: 0 > 1") 25 | shouldContain( 26 | errors("val a: List[String] Refined Size[GreaterThan[1]] = List(\"a\")"), 27 | "Validation failed: list size doesn't hold predicate: 1 > 1") 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /core/src/test/scala/pl/msitko/refined/RuntimeIntSpec.scala: -------------------------------------------------------------------------------- 1 | package pl.msitko.refined 2 | 3 | import pl.msitko.refined.auto._ 4 | 5 | class RuntimeIntSpec extends munit.FunSuite { 6 | 7 | test("should work for GreaterThan") { 8 | assertEquals(Refined.refineV[GreaterThan[5]](6), Right(Refined.unsafeApply[Int, GreaterThan[5]](6))) 9 | } 10 | 11 | test("should work for GreaterThan - negative cases") { 12 | assertEquals(Refined.refineV[GreaterThan[5]](5), Left("Validation of refined type failed: 5 > 5")) 13 | assertEquals(Refined.refineV[GreaterThan[5]](4), Left("Validation of refined type failed: 4 > 5")) 14 | } 15 | 16 | test("should work for Or") { 17 | type Pred = Or[GreaterThan[100], LowerThan[10]] 18 | val res = Refined.refineV[Pred](9) 19 | assertEquals(res, Right(Refined.unsafeApply[Int, Pred](9))) 20 | val res2 = Refined.refineV[Pred](101) 21 | assertEquals(res2, Right(Refined.unsafeApply[Int, Pred](101))) 22 | } 23 | 24 | test("should work for Or - negative cases") { 25 | type Pred = Or[GreaterThan[100], LowerThan[10]] 26 | val res = Refined.refineV[Pred](10) 27 | assertEquals(res, Left("Validation of refined type failed: (10 > 100 Or 10 < 10)")) 28 | val res2 = Refined.refineV[Pred](100) 29 | assertEquals(res2, Left("Validation of refined type failed: (100 > 100 Or 100 < 10)")) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /core/src/test/scala/pl/msitko/refined/StringSpec.scala: -------------------------------------------------------------------------------- 1 | package pl.msitko.refined 2 | 3 | import pl.msitko.refined.Refined.* 4 | import pl.msitko.refined.auto.* 5 | import pl.msitko.refined.compiletime.ValidateExprString.EndsWith 6 | import pl.msitko.refined.testUtils.CompileTimeSuite 7 | 8 | import scala.annotation.nowarn 9 | import scala.compiletime.testing.typeCheckErrors as errors 10 | 11 | class StringSpec extends CompileTimeSuite { 12 | 13 | test("""StartsWith["abc] should fail for incorrect input""") { 14 | shouldContain( 15 | errors("""mkValidatedString["abd", StartsWith["abc"]]("abd")"""), 16 | "Validation failed: abd.startsWith(abc)") 17 | } 18 | 19 | test("""StartsWith["abc] should pass""") { 20 | val a: String Refined StartsWith["abc"] = mkValidatedString["abcd", StartsWith["abc"]]("abcd") 21 | assertEquals(a + "", "abcd") 22 | val a2: String Refined StartsWith["abc"] = mkValidatedString["abc", StartsWith["abc"]]("abc") 23 | assertEquals(a2 + "", "abc") 24 | } 25 | 26 | test("should work with Or") { 27 | val a: String Refined Or[StartsWith["abc"], EndsWith["xyz"]] = "abcd" 28 | assertEquals(a + "", "abcd") 29 | val a2: String Refined Or[StartsWith["abc"], EndsWith["xyz"]] = "axyz" 30 | assertEquals(a2 + "", "axyz") 31 | } 32 | 33 | test("should work with Or - negative cases") { 34 | shouldContain( 35 | errors("""val a: String Refined Or[StartsWith["abc"], EndsWith["xyz"]] = "abyz""""), 36 | "Validation failed: (abyz.startsWith(abc) Or abyz.endsWith(xyz)), predicate failed: abyz.endsWith(xyz)") 37 | } 38 | 39 | test("should work with And") { 40 | @nowarn("msg=unused local definition") 41 | val a: String Refined And[StartsWith["abc"], EndsWith["xyz"]] = "abcdxyz" 42 | @nowarn("msg=unused local definition") 43 | val a2: String Refined Or[StartsWith["abc"], EndsWith["xyz"]] = "axcxyz" 44 | } 45 | 46 | test("should work with And - negative cases") { 47 | shouldContain( 48 | errors("""val a: String Refined And[StartsWith["abc"], EndsWith["xyz"]] = "abyz""""), 49 | "Validation failed: (abyz.startsWith(abc) And abyz.endsWith(xyz)), predicate failed: abyz.startsWith(abc)") 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /core/src/test/scala/pl/msitko/refined/macros/ListMacrosSpec.scala: -------------------------------------------------------------------------------- 1 | package pl.msitko.refined.macros 2 | 3 | import pl.msitko.refined.testUtils.CompileTimeSuite 4 | 5 | import scala.annotation.nowarn 6 | import scala.compiletime.testing.typeCheckErrors as errors 7 | 8 | final case class TestData(a: Int, b: String) 9 | 10 | class ListMacrosSpec extends CompileTimeSuite { 11 | 12 | test("should work for non-empty lists") { 13 | assertEquals(ListMacros.listSize(List(1, 2, 3)), 3) 14 | assertEquals(ListMacros.listSize(List("a", "b")), 2) 15 | assertEquals( 16 | ListMacros.listSize(List(TestData(0, "ab"), TestData(1, "ab"), TestData(2, "ab"), TestData(3, "ab"))), 17 | 4) 18 | } 19 | 20 | test("should work for empty lists") { 21 | assertEquals(ListMacros.listSize(List.empty[Int]), 0) 22 | assertEquals(ListMacros.listSize(Nil), 0) 23 | assertEquals(ListMacros.listSize(scala.Nil), 0) 24 | } 25 | 26 | test("should fail compilation in case list size cannot be determined at compile time") { 27 | val e1 = errors("ListMacros.listSize(List.fill(10)(\"abc\"))") 28 | assert(clue(e1.head.message).startsWith("Cannot determine size of list in compiletime")) 29 | } 30 | 31 | test("should fail compilation in case list size cannot be determined at compile time (2)") { 32 | @nowarn("msg=unused local definition") 33 | val xs = List("a", "b") 34 | val e1 = errors("ListMacros.listSize(xs)") 35 | assert(clue(e1.head.message).startsWith("Cannot determine size of list in compiletime")) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /core/src/test/scala/pl/msitko/refined/testUtils/CompileTimeSuite.scala: -------------------------------------------------------------------------------- 1 | package pl.msitko.refined.testUtils 2 | 3 | import scala.compiletime.testing.typeCheckErrors 4 | import scala.compiletime.testing.Error 5 | 6 | trait CompileTimeSuite extends munit.FunSuite { 7 | 8 | def shouldContain(errors: List[Error], expectedError: String) = 9 | assert(clue(errors.map(_.message)).contains(clue(expectedError))) 10 | 11 | } 12 | -------------------------------------------------------------------------------- /project/Common.scala: -------------------------------------------------------------------------------- 1 | import com.softwaremill.SbtSoftwareMillCommon.commonSmlBuildSettings 2 | import com.softwaremill.Publish.ossPublishSettings 3 | import org.scalafmt.sbt.ScalafmtPlugin.autoImport.scalafmtOnCompile 4 | import sbt.Keys._ 5 | import sbt._ 6 | import sbt.{Compile, Project, Test, TestFramework} 7 | import xerial.sbt.Sonatype.GitHubHosting 8 | import xerial.sbt.Sonatype.autoImport.{sonatypeCredentialHost, sonatypeProfileName, sonatypeProjectHosting, sonatypeRepository} 9 | 10 | object Common { 11 | implicit class ProjectFrom(project: Project) { 12 | def commonSettings(nameArg: String): Project = project.settings( 13 | name := nameArg, 14 | organization := "pl.msitko", 15 | 16 | scalaVersion := "3.3.1", 17 | scalafmtOnCompile := true, 18 | 19 | commonSmlBuildSettings, 20 | ossPublishSettings ++ Seq( 21 | sonatypeProfileName := "pl.msitko", 22 | organizationHomepage := Some(url("https://github.com/note")), 23 | homepage := Some(url("https://github.com/note/mini-refined")), 24 | sonatypeProjectHosting := Some( 25 | GitHubHosting("note", name.value, "pierwszy1@gmail.com") 26 | ), 27 | licenses := Seq("MIT" -> url("https://opensource.org/licenses/MIT")), 28 | developers := List( 29 | Developer( 30 | id = "note", 31 | name = "Michal Sitko", 32 | email = "pierwszy1@gmail.com", 33 | url = new URL("https://github.com/note") 34 | ) 35 | ), 36 | sonatypeCredentialHost := "oss.sonatype.org", 37 | ), 38 | scalacOptions ++= Seq( 39 | "-Xfatal-warnings", 40 | ), 41 | Compile / console / scalacOptions ~= filterConsoleScalacOptions, 42 | Test / console / scalacOptions ~= filterConsoleScalacOptions, 43 | testFrameworks += new TestFramework("munit.Framework") 44 | ) 45 | } 46 | 47 | val filterConsoleScalacOptions = { options: Seq[String] => 48 | options.filterNot(Set( 49 | "-Xfatal-warnings", 50 | "-Werror", 51 | "-Wdead-code", 52 | "-Wunused:imports", 53 | "-Ywarn-unused:imports", 54 | "-Ywarn-unused-import", 55 | "-Ywarn-dead-code", 56 | )) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Dependencies { 4 | val circeVersion = "0.14.6" 5 | 6 | lazy val circe = Seq( 7 | "io.circe" %% "circe-core" % circeVersion, 8 | "io.circe" %% "circe-generic" % circeVersion % Test, 9 | "io.circe" %% "circe-parser" % circeVersion % Test 10 | ) 11 | 12 | lazy val munit = "org.scalameta" %% "munit" % "0.7.29" % Test 13 | 14 | lazy val testDeps = Seq(munit) 15 | } 16 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.9.6 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-common" % "2.0.17") 2 | addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-publish" % "2.0.17") 3 | --------------------------------------------------------------------------------