Skip to content

Commit

Permalink
Add RefinedTypeOps for companion objects of refined types (#369)
Browse files Browse the repository at this point in the history
Closes #342
  • Loading branch information
fthomas authored Dec 17, 2017
1 parent 7c8df7b commit 390f685
Show file tree
Hide file tree
Showing 10 changed files with 205 additions and 9 deletions.
3 changes: 1 addition & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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.*")
)
}
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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. */
Expand All @@ -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]
}
Original file line number Diff line number Diff line change
@@ -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}
Expand All @@ -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) */
Expand Down
Original file line number Diff line number Diff line change
@@ -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. */
Expand All @@ -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]
}
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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).*(?<!\\s)"""`.T]

object TrimmedString extends RefinedTypeOps[TrimmedString, String]
}
Original file line number Diff line number Diff line change
@@ -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.numeric.Interval

/** Module for date and time related refined types. */
Expand All @@ -12,21 +12,33 @@ trait TimeTypes {
/** An `Int` in the range from 1 to 12 representing the month-of-year. */
type Month = Int Refined Interval.Closed[W.`1`.T, W.`12`.T]

object Month extends RefinedTypeOps[Month, Int]

/**
* An `Int` in the range from 1 to 31 representing the day-of-month.
* Note that the days from 29 to 31 are not valid for all months.
*/
type Day = Int Refined Interval.Closed[W.`1`.T, W.`31`.T]

object Day extends RefinedTypeOps[Day, Int]

/** An `Int` in the range from 0 to 23 representing the hour-of-day. */
type Hour = Int Refined Interval.Closed[W.`0`.T, W.`23`.T]

object Hour extends RefinedTypeOps[Hour, Int]

/** An `Int` in the range from 0 to 59 representing the minute-of-hour. */
type Minute = Int Refined Interval.Closed[W.`0`.T, W.`59`.T]

object Minute extends RefinedTypeOps[Minute, Int]

/** An `Int` in the range from 0 to 59 representing the second-of-minute. */
type Second = Int Refined Interval.Closed[W.`0`.T, W.`59`.T]

object Second extends RefinedTypeOps[Second, Int]

/** An `Int` in the range from 0 to 999 representing the millisecond-of-second. */
type Millis = Int Refined Interval.Closed[W.`0`.T, W.`999`.T]

object Millis extends RefinedTypeOps[Millis, Int]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package eu.timepit.refined.types

import eu.timepit.refined.types.numeric.PosInt
import org.scalacheck.Prop._
import org.scalacheck.Properties

class NumericTypesSpec extends Properties("NumericTypes") {

property("PosInt.from(1)") = secure {
PosInt.from(1) ?= Right(PosInt(1))
}

property("PosInt.from(-1)") = secure {
PosInt.from(-1) ?= Left("Predicate failed: (-1 > 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))
}
}
28 changes: 26 additions & 2 deletions notes/0.8.5.markdown
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand All @@ -39,11 +60,14 @@
* 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
[#351]: https://github.com/fthomas/refined/pull/351
[#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

0 comments on commit 390f685

Please sign in to comment.