Skip to content

Commit

Permalink
Json formats for refined types
Browse files Browse the repository at this point in the history
  • Loading branch information
btlines committed Oct 13, 2017
1 parent 468a903 commit 76059c6
Show file tree
Hide file tree
Showing 9 changed files with 237 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
@@ -1,2 +1,4 @@
*.class
*.log
target/
project/target
11 changes: 11 additions & 0 deletions .travis.yml
@@ -0,0 +1,11 @@
dist: trusty
language: scala
jdk: openjdk8
scala:
- 2.12.3

script:
- sbt ++$TRAVIS_SCALA_VERSION clean coverage test coverageReport

after_success:
- bash <(curl -s https://codecov.io/bash)
70 changes: 70 additions & 0 deletions README.md
@@ -0,0 +1,70 @@
[![Build status](https://api.travis-ci.org/btlines/play-json-refined.svg?branch=master)](https://travis-ci.org/btlines/play-json-refined)
[![codecov](https://codecov.io/gh/btlines/play-json-refined/branch/master/graph/badge.svg)](https://codecov.io/gh/btlines/play-json-refined)
[![License](https://img.shields.io/:license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![Download](https://api.bintray.com/packages/beyondthelines/maven/play-json-refined/images/download.svg) ](https://bintray.com/beyondthelines/maven/play-json-refined/_latestVersion)

# Play JSON Refined

A tiny library providing Json formats for [refined](https://github.com/fthomas/refined) types.

## Context

Refined types come in handy to limit the valid values accepted in a function. However as values are often not available at compile time you need to validate them as soon as they enter your application.

In case of JSON inputs we need a format to convert from JSON to a refined type. (e.g. We want to read a non empty list or a positive integer directly from JSON).

## Setup

In order to use play-json-refined you need to add the following lines to your `build.sbt`:

```scala
resolvers += Resolver.bintrayRepo("beyondthelines", "maven")

libraryDependencies += "beyondthelines" %% "play-json-refined" % "0.0.1"
```

## Dependencies

Play JSON Refined has only 2 dependencies: [Play-json](https://github.com/playframework/play-json) and [Refined](https://github.com/fthomas/refined).

## Usage

In order to use Play JSON with refined types you need to import the following:

```scala
import eu.timepit.refined.api.Refined
import eu.timepit.refined.auto._
import play.api.libs.json._
import play.json.refined._
```

Importing play.json.refined._ adds implicit definitions to derive Json formats for refined types.

## Example

Let's take a basic example to illustrate the usage:

```scala
// import the refined that you need
// here we use numeric and collection
import eu.timepit.refined.collection._
import eu.timepit.refined.numeric._

type PosInt = Int Refined Positive
type NonEmptyString = String Refined NonEmpty

final case class Data(
i: PosInt,
s: NonEmptyString
)

implicit val dataFormat: OFormat[Data] =
Json.format[Data]

val data = Data(1, "a")
// convert to JSON
val json = Json.toJson(data)
// convert from JSON
val parsed = json.as[Data]
```

22 changes: 22 additions & 0 deletions build.sbt
@@ -0,0 +1,22 @@
scalaVersion := "2.12.3"

name := "play-json-refined"

version := "0.0.1"

organization := "beyondthelines"

resolvers += Resolver.bintrayRepo("beyondthelines", "maven")

libraryDependencies ++= Seq(
"com.typesafe.play" %% "play-json" % "2.6.6",
"eu.timepit" %% "refined" % "0.8.4",
"org.scalatest" %% "scalatest" % "3.0.4" % "test",
"org.scalacheck" %% "scalacheck" % "1.13.4" % "test"
)

licenses := ("MIT", url("http://opensource.org/licenses/MIT")) :: Nil

bintrayOrganization := Some("beyondthelines")

bintrayPackageLabels := Seq("scala", "json", "play", "refined")
2 changes: 2 additions & 0 deletions project/build.properties
@@ -0,0 +1,2 @@
sbt.version=1.0.2

3 changes: 3 additions & 0 deletions project/plugins.sbt
@@ -0,0 +1,3 @@
addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.1")

addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1")
29 changes: 29 additions & 0 deletions src/main/scala/play/json/refined/package.scala
@@ -0,0 +1,29 @@
package play.json

import eu.timepit.refined.api.{Refined, Validate}
import eu.timepit.refined.refineV
import play.api.libs.json.{JsError, JsSuccess, Reads, Writes}
import play.api.libs.functional.syntax._

package object refined {

implicit def refinedReads[T, P](
implicit reads: Reads[T], validate: Validate[T, P]
): Reads[T Refined P] =
Reads[T Refined P] { json =>
reads
.reads(json)
.flatMap { t: T =>
refineV[P](t) match {
case Left(error) => JsError(error)
case Right(value) => JsSuccess(value)
}
}
}

implicit def refinedWrites[T, P](
implicit writes: Writes[T]
): Writes[T Refined P] =
writes.contramap(_.value)

}
21 changes: 21 additions & 0 deletions src/test/scala/play/json/refined/ArbitraryRefined.scala
@@ -0,0 +1,21 @@
package play.json.refined

import eu.timepit.refined.api.{Refined, Validate}
import eu.timepit.refined.refineV
import org.scalacheck.{Arbitrary, Gen}

object ArbitraryRefined {

implicit def refinedArbitrary[T, P](
implicit arbitrary: Arbitrary[T], validate: Validate[T, P]
): Arbitrary[T Refined P] =
Arbitrary(
arbitrary.arbitrary.flatMap { t: T =>
refineV[P](t) match {
case Right(value) => Gen.const(value)
case _ => Gen.fail
}
}
)

}
77 changes: 77 additions & 0 deletions src/test/scala/play/json/refined/PlayJsonRefinedSpec.scala
@@ -0,0 +1,77 @@
package play.json.refined

import eu.timepit.refined.api.Refined
import eu.timepit.refined.auto._
import eu.timepit.refined.collection.NonEmpty
import eu.timepit.refined.numeric.Positive
import org.scalatest.{Matchers, WordSpec}
import org.scalatest.prop.GeneratorDrivenPropertyChecks._
import play.api.libs.json.{JsError, JsSuccess, Json, OFormat}
import org.scalacheck.{Arbitrary, Gen}

import ArbitraryRefined._

object PlayJsonRefinedSpec {
type NonEmptyList[A] = List[A] Refined NonEmpty
type NonEmptyString = String Refined NonEmpty
type PosInt = Int Refined Positive

final case class Data(
s: NonEmptyString,
n: PosInt,
xs: NonEmptyList[Int]
)

implicit val dataArbitrary: Arbitrary[Data] =
Arbitrary(Gen.resultOf(Data))

implicit val dataFormat: OFormat[Data] = Json.format[Data]
}

class PlayJsonRefinedSpec extends WordSpec with Matchers {

import PlayJsonRefinedSpec._

"play.json.refined" should {
"read non empty list" in forAll { list: List[String] =>
val json = Json.toJson(list)
json.validate[NonEmptyList[String]] match {
case JsSuccess(nel, _) => (nel: List[String]) shouldBe list
case JsError(_) => list shouldBe empty
}
}
"read non empty string" in forAll { s: String =>
val json = Json.toJson(s)
json.validate[NonEmptyString] match {
case JsSuccess(t, _) => (t: String) shouldBe s
case JsError(_) => s shouldBe empty
}
}
"read positive ints" in forAll { n: Int =>
val json = Json.toJson(n)
json.validate[PosInt] match {
case JsSuccess(m, _) => (m: Int) shouldBe n
case JsError(_) => n shouldBe <= (0)
}
}

"write non empty list" in forAll { list: NonEmptyList[String] =>
val json = Json.toJson(list)
json.as[List[String]] shouldBe (list: List[String])
}
"write non empty string" in forAll { s: NonEmptyString =>
val json = Json.toJson(s)
json.as[String] shouldBe (s: String)
}
"write positive ints" in forAll { n: PosInt =>
val json = Json.toJson(n)
json.as[Int] shouldBe (n: Int)
}

"format case classes with refined members" in forAll { data: Data =>
val json = Json.toJson(data)
json.as[Data] shouldBe data
}
}

}

0 comments on commit 76059c6

Please sign in to comment.