diff --git a/build.sbt b/build.sbt index 5f34af6d0..4456ab88d 100644 --- a/build.sbt +++ b/build.sbt @@ -271,8 +271,7 @@ lazy val moduleJvmSettings = Def.settings( ProblemFilters.exclude[MissingClassProblem]("eu.timepit.refined.scalacheck.util.OurMath$"), ProblemFilters.exclude[ReversedMissingMethodProblem]( "eu.timepit.refined.StringValidate.ipv4Validate"), - ProblemFilters.exclude[ReversedMissingMethodProblem]( - "eu.timepit.refined.types.NetTypes.PrivateNetworks") + ProblemFilters.exclude[ReversedMissingMethodProblem]("eu.timepit.refined.types.*") ) } ) diff --git a/modules/core/shared/src/main/scala/eu/timepit/refined/api/RefinedType.scala b/modules/core/shared/src/main/scala/eu/timepit/refined/api/RefinedType.scala new file mode 100644 index 000000000..a513d8fc2 --- /dev/null +++ b/modules/core/shared/src/main/scala/eu/timepit/refined/api/RefinedType.scala @@ -0,0 +1,57 @@ +package eu.timepit.refined +package api + +/** + * Type class that combines `[[RefType]]` and `[[Validate]]` instances + * for a refined type `FTP`. + */ +trait RefinedType[FTP] { + type F[_, _] + type T + type P + + val refType: RefType[F] + + val validate: Validate[T, P] + + val alias: F[T, P] =:= FTP + + final def refine(t: T): Either[String, FTP] = { + val res = validate.validate(t) + if (res.isPassed) Right(alias(refType.unsafeWrap(t))) + else Left(validate.showResult(t, res)) + } + + final def unsafeRefine(t: T): FTP = + refine(t).fold(err => throw new IllegalArgumentException(err), identity) +} + +object RefinedType { + + def apply[FTP](implicit rt: RefinedType[FTP]): Aux[FTP, rt.F, rt.T, rt.P] = rt + + type Aux[FTP, F0[_, _], T0, P0] = RefinedType[FTP] { + type F[x, y] = F0[x, y] + type T = T0 + type P = P0 + } + + type AuxT[FTP, T0] = RefinedType[FTP] { + type T = T0 + } + + implicit def instance[F0[_, _], T0, P0]( + implicit + rt: RefType[F0], + v: Validate[T0, P0] + ): Aux[F0[T0, P0], F0, T0, P0] = + new RefinedType[F0[T0, P0]] { + override type F[x, y] = F0[x, y] + override type T = T0 + override type P = P0 + + override val refType: RefType[F] = rt + override val validate: Validate[T, P] = v + override val alias: F[T, P] =:= F0[T0, P0] = implicitly + } +} diff --git a/modules/core/shared/src/main/scala/eu/timepit/refined/api/RefinedTypeOps.scala b/modules/core/shared/src/main/scala/eu/timepit/refined/api/RefinedTypeOps.scala new file mode 100644 index 000000000..b5084b3d2 --- /dev/null +++ b/modules/core/shared/src/main/scala/eu/timepit/refined/api/RefinedTypeOps.scala @@ -0,0 +1,41 @@ +package eu.timepit.refined +package api + +/** + * Provides functions to create values of the refined type `FTP` from + * values of the base type `T`. It is intended to simplify the definition + * of a refined type's companion object. + * + * Example: {{{ + * scala> import eu.timepit.refined.api.{ Refined, RefinedTypeOps } + * | import eu.timepit.refined.numeric.Positive + * + * scala> type PosInt = Int Refined Positive + * + * scala> object PosInt extends RefinedTypeOps[PosInt, Int] + * + * scala> PosInt(1) + * res0: PosInt = 1 + * + * scala> PosInt.from(2) + * res1: Either[String, PosInt] = Right(2) + * }}} + */ +class RefinedTypeOps[FTP, T](implicit rt: RefinedType.AuxT[FTP, T]) { + + def apply[F[_, _], P](t: T)( + implicit ev: F[T, P] =:= FTP, + rt: RefType[F], + v: Validate[T, P] + ): FTP = + macro macros.RefineMacro.implApplyRef[FTP, F, T, P] + + def from(t: T): Either[String, FTP] = + rt.refine(t) + + def unapply(t: T): Option[FTP] = + from(t).right.toOption + + def unsafeFrom(t: T): FTP = + rt.unsafeRefine(t) +} diff --git a/modules/core/shared/src/main/scala/eu/timepit/refined/types/char.scala b/modules/core/shared/src/main/scala/eu/timepit/refined/types/char.scala index 75267cb17..3dbaa0e98 100644 --- a/modules/core/shared/src/main/scala/eu/timepit/refined/types/char.scala +++ b/modules/core/shared/src/main/scala/eu/timepit/refined/types/char.scala @@ -1,6 +1,6 @@ package eu.timepit.refined.types -import eu.timepit.refined.api.Refined +import eu.timepit.refined.api.{Refined, RefinedTypeOps} import eu.timepit.refined.char.{LowerCase, UpperCase} /** Module for `Char` refined types. */ @@ -11,6 +11,10 @@ trait CharTypes { /** A `Char` that is a lower case character. */ type LowerCaseChar = Char Refined LowerCase + object LowerCaseChar extends RefinedTypeOps[LowerCaseChar, Char] + /** A `Char` that is an upper case character. */ type UpperCaseChar = Char Refined UpperCase + + object UpperCaseChar extends RefinedTypeOps[UpperCaseChar, Char] } diff --git a/modules/core/shared/src/main/scala/eu/timepit/refined/types/net.scala b/modules/core/shared/src/main/scala/eu/timepit/refined/types/net.scala index c760fec25..c436efdec 100644 --- a/modules/core/shared/src/main/scala/eu/timepit/refined/types/net.scala +++ b/modules/core/shared/src/main/scala/eu/timepit/refined/types/net.scala @@ -1,7 +1,7 @@ package eu.timepit.refined.types import eu.timepit.refined.W -import eu.timepit.refined.api.Refined +import eu.timepit.refined.api.{Refined, RefinedTypeOps} import eu.timepit.refined.boolean.{And, Or} import eu.timepit.refined.numeric.Interval import eu.timepit.refined.string.{IPv4, MatchesRegex, StartsWith} @@ -14,18 +14,28 @@ trait NetTypes { /** An `Int` in the range from 0 to 65535 representing a port number. */ type PortNumber = Int Refined Interval.Closed[W.`0`.T, W.`65535`.T] + object PortNumber extends RefinedTypeOps[PortNumber, Int] + /** An `Int` in the range from 0 to 1023 representing a port number. */ type SystemPortNumber = Int Refined Interval.Closed[W.`0`.T, W.`1023`.T] + object SystemPortNumber extends RefinedTypeOps[SystemPortNumber, Int] + /** An `Int` in the range from 1024 to 49151 representing a port number. */ type UserPortNumber = Int Refined Interval.Closed[W.`1024`.T, W.`49151`.T] + object UserPortNumber extends RefinedTypeOps[UserPortNumber, Int] + /** An `Int` in the range from 49152 to 65535 representing a port number. */ type DynamicPortNumber = Int Refined Interval.Closed[W.`49152`.T, W.`65535`.T] + object DynamicPortNumber extends RefinedTypeOps[DynamicPortNumber, Int] + /** An `Int` in the range from 1024 to 65535 representing a port number. */ type NonSystemPortNumber = Int Refined Interval.Closed[W.`1024`.T, W.`65535`.T] + object NonSystemPortNumber extends RefinedTypeOps[NonSystemPortNumber, Int] + import PrivateNetworks._ /** A `String` representing a valid IPv4 in the private network 10.0.0.0/8 (RFC1918) */ diff --git a/modules/core/shared/src/main/scala/eu/timepit/refined/types/numeric.scala b/modules/core/shared/src/main/scala/eu/timepit/refined/types/numeric.scala index 8b16b7af8..efe0f8202 100644 --- a/modules/core/shared/src/main/scala/eu/timepit/refined/types/numeric.scala +++ b/modules/core/shared/src/main/scala/eu/timepit/refined/types/numeric.scala @@ -1,6 +1,6 @@ package eu.timepit.refined.types -import eu.timepit.refined.api.Refined +import eu.timepit.refined.api.{Refined, RefinedTypeOps} import eu.timepit.refined.numeric.{Negative, NonNegative, NonPositive, Positive} /** Module for numeric refined types. */ @@ -11,24 +11,40 @@ trait NumericTypes { /** An `Int` in the range from 1 to `Int.MaxValue`. */ type PosInt = Int Refined Positive + object PosInt extends RefinedTypeOps[PosInt, Int] + /** An `Int` in the range from 0 to `Int.MaxValue`. */ type NonNegInt = Int Refined NonNegative + object NonNegInt extends RefinedTypeOps[NonNegInt, Int] + /** An `Int` in the range from `Int.MinValue` to -1. */ type NegInt = Int Refined Negative + object NegInt extends RefinedTypeOps[NegInt, Int] + /** An `Int` in the range from `Int.MinValue` to 0. */ type NonPosInt = Int Refined NonPositive + object NonPosInt extends RefinedTypeOps[NonPosInt, Int] + /** A `Long` in the range from 1 to `Long.MaxValue`. */ type PosLong = Long Refined Positive + object PosLong extends RefinedTypeOps[PosLong, Long] + /** A `Long` in the range from 0 to `Long.MaxValue`. */ type NonNegLong = Long Refined NonNegative + object NonNegLong extends RefinedTypeOps[NonNegLong, Long] + /** A `Long` in the range from `Long.MinValue` to -1. */ type NegLong = Long Refined Negative + object NegLong extends RefinedTypeOps[NegLong, Long] + /** A `Long` in the range from `Long.MinValue` to 0. */ type NonPosLong = Long Refined NonPositive + + object NonPosLong extends RefinedTypeOps[NonPosLong, Long] } diff --git a/modules/core/shared/src/main/scala/eu/timepit/refined/types/string.scala b/modules/core/shared/src/main/scala/eu/timepit/refined/types/string.scala index aa3075e2d..74ee79dfb 100644 --- a/modules/core/shared/src/main/scala/eu/timepit/refined/types/string.scala +++ b/modules/core/shared/src/main/scala/eu/timepit/refined/types/string.scala @@ -1,7 +1,7 @@ package eu.timepit.refined.types import eu.timepit.refined.W -import eu.timepit.refined.api.Refined +import eu.timepit.refined.api.{Refined, RefinedTypeOps} import eu.timepit.refined.collection.NonEmpty import eu.timepit.refined.string.MatchesRegex @@ -13,6 +13,10 @@ trait StringTypes { /** A `String` that is not empty. */ type NonEmptyString = String Refined NonEmpty + object NonEmptyString extends RefinedTypeOps[NonEmptyString, String] + /** A `String` that contains no leading or trailing whitespace. */ type TrimmedString = String Refined MatchesRegex[W.`"""^(?!\\s).*(? 0).") + } + + property("PosInt.unapply(1)") = secure { + val PosInt(x) = 1 + x ?= PosInt(1) + } + + property("PosInt.unsafeFrom(1)") = secure { + PosInt.unsafeFrom(1) ?= PosInt(1) + } + + property("PosInt.unsafeFrom(-1)") = secure { + throws(classOf[IllegalArgumentException])(PosInt.unsafeFrom(-1)) + } +} diff --git a/notes/0.8.5.markdown b/notes/0.8.5.markdown index 5e4c50ba0..fce2bd08a 100644 --- a/notes/0.8.5.markdown +++ b/notes/0.8.5.markdown @@ -1,7 +1,26 @@ ### New features -* Add a `TrimmedString` refined type for `String`s without leading or - trailing whitespace. ([#275][#275]) +* Add `RefinedTypeOps` to simplify the definition of a refined type's + companion object and the `RefinedType` type class which combines + `RefType` and `Validate` instances for a refined type. With the + former a companion of a refined type can now defined like this: + ```scala + // definition of the refined type `PosInt` + scala> type PosInt = Int Refined Positive + + // definition of the companion object for `PosInt` + scala> object PosInt extends RefinedTypeOps[PosInt, Int] + + scala> PosInt(1) // create `PosInt`s from literals + res0: PosInt = 1 + + scala> PosInt.from(2) // create `PosInt`s from runtime values + res1: Either[String, PosInt] = Right(2) + ``` + This change also adds companion objects for the refined types in + the `eu.timepit.refined.types` package which simplifies creating + values of these types a lot. + ([#342][#342], [#369][#369], [#193][#193]) * Support `BigInt` and `BigDecimal` literals in compile-time checks. For example, this is now possible: ```scala @@ -26,6 +45,8 @@ res0: Either[String, ByteVector Refined Size[Equal[Long(2L)]]] = Right(ByteVector(2 bytes, 0xabcd)) ``` ([#365][#365]) +* Add a `TrimmedString` refined type for `String`s without leading or + trailing whitespace. ([#275][#275]) ### Changes @@ -39,7 +60,9 @@ * Update to Scala.js 0.6.21. ([#351][#351]) * Update `refined-scalaz` to Scalaz 7.2.17. ([#360][#360]) +[#193]: https://github.com/fthomas/refined/pull/193 [#275]: https://github.com/fthomas/refined/pull/275 +[#342]: https://github.com/fthomas/refined/issues/342 [#343]: https://github.com/fthomas/refined/pull/343 [#345]: https://github.com/fthomas/refined/pull/345 [#350]: https://github.com/fthomas/refined/pull/350 @@ -47,3 +70,4 @@ [#356]: https://github.com/fthomas/refined/pull/356 [#360]: https://github.com/fthomas/refined/pull/360 [#365]: https://github.com/fthomas/refined/pull/365 +[#369]: https://github.com/fthomas/refined/pull/369