Skip to content
This repository has been archived by the owner on Jun 15, 2020. It is now read-only.

Commit

Permalink
Merge pull request #62 from OlivierBlanvillain/json-ast
Browse files Browse the repository at this point in the history
Replace json4s-ast with custom AST
  • Loading branch information
jto authored Jun 24, 2016
2 parents 96d46ab + e46f04b commit 14fb41c
Show file tree
Hide file tree
Showing 25 changed files with 375 additions and 200 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ notifications:
email:
false

script: bash misc/ci.sh
script: bash scripts/ci.sh

cache:
directories:
Expand Down
2 changes: 1 addition & 1 deletion PUBLISH.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
```sh
git checkout gh-pages
git checkout master .
sh misc/build-book.sh
sh scripts/build-book.sh
git add .
git commit -am "Update book"
git push
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,11 @@ val validationVersion = "2.0"
libraryDependencies ++= Seq(
"io.github.jto" %% "validation-core" % validationVersion,
"io.github.jto" %% "validation-playjson" % validationVersion,
"io.github.jto" %% "validation-json4s" % validationVersion,
"io.github.jto" %% "validation-jsonast" % validationVersion,
"io.github.jto" %% "validation-form" % validationVersion,
"io.github.jto" %% "validation-delimited" % validationVersion,
"io.github.jto" %% "validation-xml" % validationVersion
// "io.github.jto" %%% "validation-jsjson" % validationVersion
)
```

Expand Down
27 changes: 12 additions & 15 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,16 @@ val license = ("Apache License", url("http://www.apache.org/licenses/LICENSE-2.0
val catsVersion = "0.6.0"
val jodaConvertVersion = "1.8.1"
val jodaTimeVersion = "2.9.4"
val json4sAstVersion = "4.0.0-M1"
val kindProjectorVersion = "0.7.1"
val parserCombinatorsVersion = "1.0.2"
val playVersion = "2.5.3"
val scalacVersion = "2.11.8"
val scalatestVersion = "3.0.0-M16-SNAP6"
val scalaXmlVersion = "1.0.5"

val json4sAST = libraryDependencies += "org.json4s" %%% "json4s-ast" % json4sAstVersion

lazy val root = aggregate("validation", validationJVM, validationJS, docs).in(file("."))
lazy val validationJVM = aggregate("validationJVM", coreJVM, formJVM, delimitedJVM, json4sJVM, `validation-playjson`, `validation-xml`, `date-tests`)
lazy val validationJS = aggregate("validationJS", coreJS, formJS, delimitedJS, json4sJS, `validation-jsjson`)
lazy val validationJVM = aggregate("validationJVM", coreJVM, formJVM, delimitedJVM, jsonAstJVM, `validation-playjson`, `validation-xml`, `date-tests`)
lazy val validationJS = aggregate("validationJS", coreJS, formJS, delimitedJS, jsonAstJS, `validation-jsjson`)

lazy val `validation-core` = crossProject
.crossType(CrossType.Pure)
Expand Down Expand Up @@ -48,14 +45,15 @@ lazy val delimitedJVM = `validation-delimited`.jvm
lazy val delimitedJS = `validation-delimited`.js
lazy val delimited = aggregate("validation-delimited", delimitedJVM, delimitedJS)

lazy val `validation-json4s` = crossProject
.crossType(CrossType.Pure)
lazy val `validation-jsonast` = crossProject
.crossType(CrossType.Full)
.settings(validationSettings: _*)
.settings(json4sAST)
.dependsOn(`validation-core`)
lazy val json4sJVM = `validation-json4s`.jvm
lazy val json4sJS = `validation-json4s`.js
lazy val json4s = aggregate("validation-json4s", json4sJVM, json4sJS)
.jvmSettings(libraryDependencies +=
"com.typesafe.play" %% "play-json" % playVersion)
lazy val jsonAstJVM = `validation-jsonast`.jvm
lazy val jsonAstJS = `validation-jsonast`.js
lazy val jsonAst = aggregate("validation-jsonast", jsonAstJVM, jsonAstJS)

lazy val `validation-playjson` = project
.settings(validationSettings: _*)
Expand All @@ -80,13 +78,12 @@ lazy val docs = project
.settings(crossTarget := file(".") / "docs" / "target")
.settings(tutSettings: _*)
.settings(scalacOptions -= "-Ywarn-unused-import")
.dependsOn(coreJVM, formJVM, delimitedJVM, json4sJVM, `validation-playjson`, `validation-xml`)
.dependsOn(coreJVM, formJVM, delimitedJVM, jsonAstJVM, `validation-playjson`, `validation-xml`)

lazy val `date-tests` = project
.settings(validationSettings: _*)
.settings(dontPublish: _*)
.settings(json4sAST)
.dependsOn(coreJVM, formJVM, json4sJVM, `validation-playjson`, `validation-xml`)
.dependsOn(coreJVM, formJVM, jsonAstJVM, `validation-playjson`, `validation-xml`)

def aggregate(name: String, projects: ProjectReference*): Project =
Project(name, file("." + name))
Expand All @@ -104,7 +101,7 @@ lazy val settings = Seq(
testOptions in Test += Tests.Argument(TestFrameworks.ScalaTest, "-oDF"),
scalaJSStage in Global := FastOptStage,
parallelExecution := false
) ++ reformatOnCompileSettings
)

val commonScalacOptions = Seq(
"-deprecation",
Expand Down
7 changes: 3 additions & 4 deletions date-tests/src/test/scala/DateSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -480,9 +480,8 @@ class DateSpec extends WordSpec with Matchers {
}
}

"json4s" should {
import jto.validation.json4s._
import org.json4s.ast.safe._
"jsonAst" should {
import jto.validation.jsonast._
import Rules._, Writes._

"Format" when {
Expand Down Expand Up @@ -540,7 +539,7 @@ class DateSpec extends WordSpec with Matchers {
"time" in {
Formatting[JValue, JObject] { __ =>
(__ \ "n").format(jodaTimeR, jodaTimeW)
}.validate(JObject(Map("n" -> JNumber(dd.getTime)))) shouldBe
}.validate(JObject(Map("n" -> JNumber(dd.getTime.toString)))) shouldBe
(Valid(jd))

Formatting[JValue, JObject] { __ =>
Expand Down
2 changes: 1 addition & 1 deletion docs/src/main/tut/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@

- Impove error reporting ([#40](/~https://github.com/jto/validation/pull/40) [1](/~https://github.com/jto/validation/commit/357b87778f19fbbc06a49da08cb2dccf9e0a40e3), [2](/~https://github.com/jto/validation/commit/c1bdac7fcff098b1d85c6881c731d5fd4ee2ac2e))

- Rework project structure: `json``playjson`, `json4s``json-ast`, `jsjson` ([#50](/~https://github.com/jto/validation/pull/50) [0](/~https://github.com/jto/validation/commit/f95ac30b1d1346a27e26c08841ee06c00340891f) [1](/~https://github.com/jto/validation/commit/5b36f606334a5fe26715cf0d7c47ebf861acb811), [2](/~https://github.com/jto/validation/commit/3f31f4917d01b8f6fdefe4adaca70ddc823722db))
- Rework project structure: `json``playjson`, `json4s``jsonast`, `jsjson` ([#50](/~https://github.com/jto/validation/pull/50) [0](/~https://github.com/jto/validation/commit/f95ac30b1d1346a27e26c08841ee06c00340891f) [1](/~https://github.com/jto/validation/commit/5b36f606334a5fe26715cf0d7c47ebf861acb811), [2](/~https://github.com/jto/validation/commit/3f31f4917d01b8f6fdefe4adaca70ddc823722db))

- Add Scala.js support ([#42](/~https://github.com/jto/validation/pull/42) [1](/~https://github.com/jto/validation/commit/db359abfbe90d2b3b853beabcbabe88ecd1cfddb), [2](/~https://github.com/jto/validation/commit/568aa1fa1df06d775abb583cac8da679c1301227), [3](/~https://github.com/jto/validation/commit/d67d6dee7d99d27d6cf751cc69b83235b57a8246), [4](/~https://github.com/jto/validation/commit/67499a823ff463860b72d6697cf45b5764c475b2), [5](/~https://github.com/jto/validation/commit/d720ba265541a90f225c388043b5430a68e9fff3))
2 changes: 1 addition & 1 deletion docs/src/main/tut/V2MigrationGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ becomes
#### Package name

- Since the library does not depend on Play anymore and is not planned to be integrated into Play, the package names have changed. Basically `play.api.mapping` now becomes `jto.validation`. A simple search and replace in your project should work.
- The validation api support both json4s and play-json. Therefore, the package name for play json changes. `play.api.mapping.json` becomes `play.api.mapping.playjson`
- The validation api support several json representations. Therefore, the package name for play json changes. `play.api.mapping.json` becomes `play.api.mapping.playjson`

#### Rule renaming

Expand Down
32 changes: 17 additions & 15 deletions play-scalajs-example/build.sbt
Original file line number Diff line number Diff line change
@@ -1,39 +1,41 @@
val scalaV = "2.11.8"

val validationVersion = "2.0"

lazy val jvm = project
.in(file("jvm"))
.settings(
scalaVersion := scalaV,
scalaJSProjects := Seq(js),
pipelineStages := Seq(scalaJSProd),
libraryDependencies += "com.vmunier" %% "play-scalajs-scripts" % "0.5.0")
libraryDependencies ++= Seq(
"com.vmunier" %% "play-scalajs-scripts" % "0.5.0",
"io.github.jto" %% "validation-core" % validationVersion,
"io.github.jto" %% "validation-playjson" % validationVersion,
"io.github.jto" %% "validation-jsonast" % validationVersion))
.enablePlugins(PlayScala)
.aggregate(js)
.dependsOn(sharedJVM)
// To be used instead of `ProjectRef`s if compiled in isolation:
// .settings(libraryDependencies ++= Seq(
// "io.github.jto" %% "validation-core" % "2.0",
// "io.github.jto" %% "validation-playjson" % "2.0"))
.dependsOn(ProjectRef(file(".."), "validation-core"))
.dependsOn(ProjectRef(file(".."), "validation-playjson"))

lazy val js = project
.in(file("js"))
.settings(
scalaVersion := scalaV,
persistLauncher := true)
persistLauncher := true,
libraryDependencies ++= Seq(
"io.github.jto" %%% "validation-core" % validationVersion,
"io.github.jto" %%% "validation-jsjson" % validationVersion,
"io.github.jto" %%% "validation-jsonast" % validationVersion))
.enablePlugins(ScalaJSPlugin, ScalaJSPlay)
.dependsOn(sharedJS)
// To be used instead of `ProjectRef`s if compiled in isolation:
// .settings(libraryDependencies ++= Seq(
// "io.github.jto" %%% "validation-core" % "2.0",
// "io.github.jto" %%% "validation-jsjson" % "2.0"))
.dependsOn(ProjectRef(file(".."), "validation-core"))
.dependsOn(ProjectRef(file(".."), "validation-jsjson"))

lazy val shared = crossProject.crossType(CrossType.Pure)
.in(file("shared"))
.settings(scalaVersion := scalaV)
.settings(
scalaVersion := scalaV,
libraryDependencies ++= Seq(
"io.github.jto" %%% "validation-core" % validationVersion,
"io.github.jto" %%% "validation-jsonast" % validationVersion))
.jsConfigure(_.enablePlugins(ScalaJSPlay))

lazy val sharedJVM = shared.jvm
Expand Down
30 changes: 10 additions & 20 deletions play-scalajs-example/js/src/main/scala/Validate.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package client

import jto.validation.{ Format, Valid, Invalid, VA, Formatting }
import jto.validation.jsjson.{ Rules, Writes }
import jto.validation._
import jto.validation.jsonast.Ast
import jto.validation.jsjson._
import scala.scalajs.js
import js.annotation.JSExport
import model.User
Expand All @@ -10,25 +11,14 @@ import scala.Function.{unlift, const}
@JSExport
object Validate {
@JSExport
def user(json: js.Dynamic): js.Dictionary[Any] = {
import Rules._, Writes._
def user(json: js.Dynamic): js.Dynamic = {
import Writes._

val format =
Formatting[js.Dynamic, js.Dynamic] { __ =>
(
(__ \ "name").format(notEmpty) ~
(__ \ "age").format(min(0) |+| max(130)) ~
(__ \ "email").format(optionR(email), optionW(stringW)) ~
(__ \ "isAlive").format[Boolean]
)(User.apply, unlift(User.unapply))
}

val validated: VA[User] = format.validate(json)

js.Dictionary(
"isValid" -> validated.isValid,
"output" -> validated.fold(const(null), format.writes),
"errors" -> validated.fold(e => failureW.writes(Invalid(e)), const(null))
implicit val format: Format[js.Dynamic, js.Dynamic, User] = Format(
Ast.from andThen User.format,
Write.toWrite(User.format) andThen Ast.to
)

To[VA[User], js.Dynamic](format.validate(json))
}
}
9 changes: 3 additions & 6 deletions play-scalajs-example/jvm/app/controllers/Application.scala
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
package controllers

import jto.validation.playjson.Writes
import jto.validation.Write
import jto.validation._
import jto.validation.jsonast._
import play.api.Environment
import play.api.libs.json._
import play.api.mvc._

import model.User

class Application()(implicit environment: Environment) extends Controller {

def index = Action {
import Writes._
val write: Write[User, JsObject] = Write.gen[User, JsObject]
val write: Write[User, JsValue] = Write.toWrite(User.format) andThen Ast.to
val user: User = User("supercat", 20, Some("e@mail.com"), true)
val json: String = Json.prettyPrint(write.writes(user))
Ok(views.html.index(json))
}

}
17 changes: 17 additions & 0 deletions play-scalajs-example/shared/src/main/scala/User.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
package model

import jto.validation._
import jto.validation.jsonast._
import scala.Function.unlift

case class User(
name: String,
age: Int,
email: Option[String],
isAlive: Boolean
)

object User {
import Rules._, Writes._
implicit val format: Format[JValue, JObject, User] =
Formatting[JValue, JObject] { __ =>
(
(__ \ "name").format(notEmpty) ~
(__ \ "age").format(min(0) |+| max(130)) ~
(__ \ "email").format(optionR(email), optionW(stringW)) ~
(__ \ "isAlive").format[Boolean]
)(User.apply, unlift(User.unapply))
}
}
File renamed without changes.
4 changes: 2 additions & 2 deletions misc/ci.sh → scripts/ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ test_cmd="$sbt_cmd clean test"

coverage="$sbt_cmd clean coverage validationJVM/test coverageReport && sbt coverageAggregate && sbt coveralls"

compile_example="(cd play-scalajs-example; $sbt_cmd compile)"
compile_example="$sbt_cmd publish-local && (cd play-scalajs-example && $sbt_cmd compile)"

compile_doc="bash misc/build-book.sh"
compile_doc="bash scripts/build-book.sh"

run_cmd="$coverage && $test_cmd && $compile_example && $compile_doc"

Expand Down
10 changes: 10 additions & 0 deletions validation-jsjson/src/main/scala/Writes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,16 @@ object Writes
.asInstanceOf[js.Dynamic]
}

implicit def vaW[I](implicit w: WriteLike[I, js.Dynamic]) =
Write[VA[I], js.Dynamic] { va =>
js.Dictionary(
"isValid" -> va.isValid.asInstanceOf[js.Dynamic],
"output" -> va.fold(_ => null, w.writes),
"errors" -> va.fold(e => failureW.writes(Invalid(e)), _ => null)
)
.asInstanceOf[js.Dynamic]
}

implicit def writeJson[I](path: Path)(
implicit w: WriteLike[I, js.Dynamic]): Write[I, js.Dynamic] = Write {
i =>
Expand Down
9 changes: 9 additions & 0 deletions validation-jsjson/src/test/scala/WritesSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,15 @@ class WritesSpec extends WordSpec with Matchers with JsAnyEquality {
}
w3.writes(u1) shouldBe m1
}

"write VA" in {
val o = js.Dictionary("a" -> "string").asInstanceOf[js.Dynamic]

To[VA[js.Dynamic], js.Dynamic](Valid(o)) shouldBe js.Dictionary(
"isValid" -> true,
"output" -> o,
"errors" -> null)
}
}
}
}
56 changes: 56 additions & 0 deletions validation-jsonast/js/src/main/scala/Ast.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package jto.validation
package jsonast

import scala.scalajs.js
import scala.scalajs.js.JSConverters._

object Ast {
val to: Write[JValue, js.Dynamic] = Write[JValue, js.Any] {
case JNull => null
case JObject (value) => value.mapValues(to.writes).toJSDictionary
case JArray (value) => value.map(to.writes).toJSArray
case JBoolean(value) => value
case JString (value) => value
case JNumber (value) =>
val d = value.toDouble
if (d.isNaN || d.isInfinity) null else d
}.map(_.asInstanceOf[js.Dynamic])

private val undefined = scala.scalajs.js.undefined
private case class FunctionInJsonException(path: Path) extends Exception

private def unsafeAny2JValue(input: Any, path: Path): JValue = input match {
case null => JNull
case s: String => JString(s)
case b: Boolean => JBoolean(b)
case d: Double => JNumber(d.toString)
case `undefined` => JNull

case a: js.Array[js.Dynamic @unchecked] =>
JArray(a.map(v => unsafeAny2JValue(v, path \ 0)))

case o: js.Object =>
JObject(o.asInstanceOf[js.Dictionary[js.Dynamic]]
.map { case (k, v) => k -> unsafeAny2JValue(v, path \ k) }.toMap)

case _ =>
// This is a trade off between the various option to handle js.Function in json objects.
// We could also go one step further and return all the paths which contain functions,
// but this would imply sequence over Validated, which would throw away the perfs in
// the general case.
//
// This is what other are doing:
// - The native JSON.stringity is completely silent.
// - Circe parses then as nulls https://goo.gl/iQ0ANV.
throw new FunctionInJsonException(path)
}

val from: Rule[js.Dynamic, JValue] = Rule { j =>
try {
Valid(unsafeAny2JValue(j, Path))
} catch {
case FunctionInJsonException(path) =>
Invalid(Seq(path -> Seq(ValidationError("Json cannot contain functions."))))
}
}
}
Loading

0 comments on commit 14fb41c

Please sign in to comment.