Skip to content

Commit a93f468

Browse files
Sebastian OliveriSebastian Oliveri
Sebastian Oliveri
authored and
Sebastian Oliveri
committed
initial commit
0 parents  commit a93f468

22 files changed

+568
-0
lines changed

.gitignore

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/target
2+
.env
3+
4+
# eclipse
5+
/.classpath
6+
/.project
7+
/.settings
8+
9+
# IntelliJ IDEA
10+
/.idea
11+
*.iml
12+
13+
# Mac
14+
.DS_Store
15+
**/.DS_Store
16+
17+
# Windows
18+
Thumbs.db
19+
/bin/
20+
21+
/db
22+
/logs/
23+
/modules
24+
/project/project
25+
/project/target
26+
tmp/
27+
test-result
28+
server.pid
29+
/dist/
30+
.cache
31+
/ddata-*
32+

build.sbt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name := "json-schema-validation"
2+
3+
version := "0.1"
4+
5+
scalaVersion := "2.13.3"
6+
7+
licenses += ("MIT", url("http://opensource.org/licenses/MIT"))
8+
9+
resolvers += Resolver.bintrayRepo("fluent-assertions", "releases")
10+
11+
libraryDependencies += "nulluncertainty" %% "fluent-assertions" % "2.0.1"
12+
libraryDependencies += "com.typesafe.play" %% "play-json" % "2.9.1"
13+
libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.8" % "test"
14+
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.8" % "test"

project/build.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
sbt.version = 1.3.13
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package jsonschemavalidation
2+
3+
import org.nulluncertainty.expression.ComposableBooleanExp
4+
import play.api.libs.json.{JsObject, JsValue}
5+
6+
trait BooleanExpBuilder {
7+
8+
def build(path: Path, propertyDefinition: JsObject): ComposableBooleanExp[JsValue]
9+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package jsonschemavalidation
2+
3+
import org.nulluncertainty.expression.ComposableBooleanExp
4+
import play.api.libs.json.{JsArray, JsObject, JsValue}
5+
import play.api.libs.json.Reads._
6+
7+
object DelegatingJsBooleanExpBuilder extends BooleanExpBuilder {
8+
9+
val builders: Map[String,BooleanExpBuilder] =
10+
Map(
11+
"object" -> new JsExpectedTypeBooleanExpBuilder[JsObject](JsObjectReads, JsObjectBooleanExpBuilder),
12+
"string" -> new JsExpectedTypeBooleanExpBuilder[String](StringReads, JsStringBooleanExpBuilder),
13+
"array" -> new JsExpectedTypeBooleanExpBuilder[JsArray](JsArrayReads, JsArrayBooleanExpBuilder),
14+
"integer" -> new JsExpectedTypeBooleanExpBuilder[Int](IntReads, JsNumericBooleanExpBuilder),
15+
"number" -> new JsExpectedTypeBooleanExpBuilder[BigDecimal](bigDecReads, JsNumericBooleanExpBuilder)
16+
)
17+
18+
override def build(path: Path, definition: JsObject): ComposableBooleanExp[JsValue] =
19+
builders(definition.\("type").as[String]).build(path, definition)
20+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package jsonschemavalidation
2+
3+
import org.nulluncertainty.expression.ComposableAssertionExp
4+
import play.api.libs.json.{JsArray, JsObject, JsValue}
5+
import play.api.libs.json.Reads._
6+
7+
object DelegatingJsValidationBuilder extends JsValidationBuilder {
8+
9+
val builders: Map[String,JsValidationBuilder] =
10+
Map(
11+
"object" -> new JsExpectedTypeValidationBuilder[JsObject](JsObjectReads, JsObjectValidationBuilder),
12+
"string" -> new JsExpectedTypeValidationBuilder[String](StringReads, JsStringValidationBuilder),
13+
"array" -> new JsExpectedTypeValidationBuilder[JsArray](JsArrayReads, JsArrayValidationBuilder),
14+
"integer" -> new JsExpectedTypeValidationBuilder[Int](IntReads, JsNumericValidationBuilder),
15+
"number" -> new JsExpectedTypeValidationBuilder[BigDecimal](bigDecReads, JsNumericValidationBuilder),
16+
"boolean" -> new JsExpectedTypeValidationBuilder[Boolean](BooleanReads, JsNullValidationBuilder),
17+
)
18+
19+
override def build(path: Path, definition: JsObject): ComposableAssertionExp[JsValue,JsValue,JsValue] =
20+
builders(definition.\("type").as[String]).build(path, definition)
21+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package jsonschemavalidation
2+
3+
import org.nulluncertainty.assertion.AssertionBuilder.assertThat
4+
import org.nulluncertainty.expression.{Bool, ComposableBooleanExp, TrueBooleanExp, TrueExp}
5+
import play.api.libs.json.{JsArray, JsObject, JsValue}
6+
7+
object JsArrayBooleanExpBuilder extends BooleanExpBuilder {
8+
9+
override def build(path: Path, arrayDefinition: JsObject): ComposableBooleanExp[JsValue] = {
10+
11+
val array: JsValue => Seq[JsValue] = path.valueIn(_).get.as[JsArray].value.toSeq
12+
13+
val itemDefinition = (arrayDefinition \ "items").as[JsObject]
14+
15+
((arrayDefinition \ "minItems").asOpt[Int].map(minItems =>
16+
assertThat({array(_:JsValue).size}).isGreaterThanOrEqualTo(minItems).expression).toList ++
17+
(arrayDefinition \ "maxItems").asOpt[Int].map(maxItems =>
18+
assertThat({array(_:JsValue).size}).isLessThanOrEqualTo(maxItems).expression).toList ++
19+
(arrayDefinition \ "uniqueItems").asOpt[Boolean].map(_ =>
20+
assertThat(array).containsNoDuplicates.expression).toList ++
21+
(arrayDefinition \ "contains").asOpt[JsObject].map(containsDefinition =>
22+
new ComposableBooleanExp[JsValue] {
23+
override def evaluate(context: JsValue): Bool =
24+
array(context).zipWithIndex.foldLeft[Bool](TrueExp) {
25+
case (bool,(_, index)) =>
26+
bool.or(DelegatingJsBooleanExpBuilder.build(path.add(index), containsDefinition).evaluate(context))
27+
}
28+
}).toList :+
29+
new ComposableBooleanExp[JsValue] {
30+
override def evaluate(context: JsValue): Bool =
31+
array(context).zipWithIndex.foldLeft[Bool](TrueExp) {
32+
case (bool, (_, index)) =>
33+
bool.and(DelegatingJsBooleanExpBuilder.build(path.add(index), itemDefinition).evaluate(context))
34+
}
35+
})
36+
.reduceOption(_ and _)
37+
.getOrElse(TrueBooleanExp())
38+
}
39+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package jsonschemavalidation
2+
3+
import org.nulluncertainty.assertion.AssertionBuilder.assertThat
4+
import org.nulluncertainty.expression.{AssertionResultBehaviour, AssertionSuccessfulResult, ComposableAssertionExp, SuccessfulAssertionExp}
5+
import play.api.libs.json.{JsArray, JsObject, JsValue}
6+
7+
object JsArrayValidationBuilder extends JsValidationBuilder {
8+
9+
override def build(path: Path, arrayDefinition: JsObject): ComposableAssertionExp[JsValue,JsValue,JsValue] = {
10+
11+
val array: JsValue => Seq[JsValue] = path.valueIn(_).get.as[JsArray].value.toSeq
12+
13+
val itemDefinition = (arrayDefinition \ "items").as[JsObject]
14+
15+
((arrayDefinition \ "minItems").asOpt[Int].map(minItems =>
16+
assertThat({array(_:JsValue).size}).isGreaterThanOrEqualTo(minItems)
17+
.otherwise[String]((js:JsValue) => s"Array ${path.toString} has ${array(js).size} items, but a minimum of $minItems is required.")).toList ++
18+
(arrayDefinition \ "maxItems").asOpt[Int].map(maxItems =>
19+
assertThat({array(_:JsValue).size}).isLessThanOrEqualTo(maxItems)
20+
.otherwise[String](s"Array ${path.toString} exceeds maximum items allowed that is $maxItems.")).toList ++
21+
(arrayDefinition \ "uniqueItems").asOpt[Boolean].map(_ =>
22+
assertThat(array).containsNoDuplicates
23+
.otherwise[String](s"Array ${path.toString} should contain no duplicates.")))
24+
.reduceOption[ComposableAssertionExp[JsValue,JsValue,JsValue]](_ ifTrue _).getOrElse(SuccessfulAssertionExp())
25+
.ifTrue {
26+
(context: JsValue) =>
27+
array(context)
28+
.zipWithIndex
29+
.foldLeft[AssertionResultBehaviour[JsValue]](AssertionSuccessfulResult(context)) {
30+
case (assertionResult,(_, index)) =>
31+
assertionResult
32+
.and(DelegatingJsValidationBuilder.build(path.add(index), itemDefinition)
33+
.evaluate(context)) }
34+
}
35+
}
36+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package jsonschemavalidation
2+
3+
import org.nulluncertainty.expression.{BooleanExp, ComposableBooleanExp}
4+
import play.api.libs.json.{JsObject, JsValue, Reads}
5+
6+
import scala.reflect.ClassTag
7+
8+
class JsExpectedTypeBooleanExpBuilder[U: ClassTag](reads: Reads[U], next: BooleanExpBuilder) extends BooleanExpBuilder {
9+
10+
override def build(path: Path, propertyDefinition: JsObject): ComposableBooleanExp[JsValue] = {
11+
val propertyValueIsExpectedType: JsValue => Boolean = path.valueIn(_).get.validate[U](reads).isSuccess
12+
13+
BooleanExp({propertyValueIsExpectedType(_)}).isTrue
14+
.ifTrue(next.build(path, propertyDefinition))
15+
}
16+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package jsonschemavalidation
2+
3+
import org.nulluncertainty.assertion.AssertionBuilder.assertThat
4+
import org.nulluncertainty.expression.ComposableAssertionExp
5+
import play.api.libs.json.{JsObject, JsValue, Reads}
6+
7+
import scala.reflect.{ClassTag, classTag}
8+
9+
class JsExpectedTypeValidationBuilder[U: ClassTag](reads: Reads[U], next: JsValidationBuilder) extends JsValidationBuilder {
10+
11+
override def build(path: Path, propertyDefinition: JsObject): ComposableAssertionExp[JsValue,JsValue,JsValue] = {
12+
13+
val propertyValueIsExpectedType: JsValue => Boolean = path.valueIn(_).get.validate[U](reads).isSuccess
14+
15+
assertThat(propertyValueIsExpectedType(_)).isTrue
16+
.otherwise[String](s"${path.toString} is expected to be ${classTag[U].runtimeClass.getSimpleName}")
17+
.ifTrue(next.build(path, propertyDefinition))
18+
}
19+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package jsonschemavalidation
2+
3+
import org.nulluncertainty.expression.{ComposableAssertionExp, SuccessfulAssertionExp}
4+
import play.api.libs.json.{JsObject, JsValue}
5+
6+
object JsNullValidationBuilder extends JsValidationBuilder {
7+
override def build(path: Path, propertyDefinition: JsObject): ComposableAssertionExp[JsValue,JsValue,JsValue] =
8+
SuccessfulAssertionExp()
9+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package jsonschemavalidation
2+
3+
import org.nulluncertainty.assertion.AssertionBuilder.assertThat
4+
import org.nulluncertainty.expression.{ComposableBooleanExp, TrueBooleanExp}
5+
import play.api.libs.json.{JsArray, JsObject, JsValue}
6+
7+
object JsNumericBooleanExpBuilder extends BooleanExpBuilder {
8+
9+
override def build(path: Path, propertyDefinition: JsObject): ComposableBooleanExp[JsValue] = {
10+
11+
val number: JsValue => BigDecimal = path.valueIn(_).get.as[BigDecimal]
12+
13+
(propertyDefinition.\("enum").asOpt[JsArray], propertyDefinition.\("const").asOpt[BigDecimal]) match {
14+
case (Some(enum), None) =>
15+
assertThat({_:JsValue => enum.value.map(_.as[BigDecimal])}).contains(number).expression
16+
case (None, Some(const)) =>
17+
assertThat(number).isEqualTo(const).expression
18+
case (None, None) =>
19+
(propertyDefinition.\("maximum").asOpt[Int]
20+
.map(maximum => assertThat(number).isLessThanOrEqualTo(maximum).expression).toList ++
21+
propertyDefinition.\("minimum").asOpt[Int]
22+
.map(minimum => assertThat(number).isGreaterThanOrEqualTo(minimum).expression).toList ++
23+
propertyDefinition.\("exclusiveMaximum").asOpt[Int]
24+
.map(exclusiveMaximum => assertThat(number).isLessThan(exclusiveMaximum).expression).toList ++
25+
propertyDefinition.\("exclusiveMinimum").asOpt[Int]
26+
.map(exclusiveMinimum => assertThat(number).isGreaterThan(exclusiveMinimum).expression))
27+
.reduceOption(_ and _)
28+
.getOrElse(TrueBooleanExp())
29+
}
30+
}
31+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package jsonschemavalidation
2+
3+
import org.nulluncertainty.assertion.AssertionBuilder.assertThat
4+
import org.nulluncertainty.expression.{ComposableAssertionExp, SuccessfulAssertionExp}
5+
import play.api.libs.json.{JsArray, JsObject, JsValue}
6+
7+
object JsNumericValidationBuilder extends JsValidationBuilder {
8+
9+
override def build(path: Path, propertyDefinition: JsObject): ComposableAssertionExp[JsValue,JsValue,JsValue] = {
10+
11+
val number: JsValue => BigDecimal = path.valueIn(_).get.as[BigDecimal]
12+
13+
(propertyDefinition.\("enum").asOpt[JsArray], propertyDefinition.\("const").asOpt[BigDecimal]) match {
14+
case (Some(enum), None) =>
15+
assertThat({ _: JsValue => enum.value.toSeq.map(_.as[BigDecimal]) }).contains(number)
16+
.otherwise[String](s"Property ${path.toString} does not match any of [${enum.value.toSeq.map(_.as[BigDecimal]).mkString(", ")}]")
17+
case (None, Some(const)) =>
18+
assertThat(number).isEqualTo(const).otherwise[String](s"${path.toString} is not equal to $const")
19+
case (None, None) =>
20+
(propertyDefinition.\("maximum").asOpt[Int]
21+
.map(maximum => assertThat(number).isLessThanOrEqualTo(maximum)
22+
.otherwise[String](s"${path.toString} exceeds maximum value of $maximum")).toList ++
23+
propertyDefinition.\("minimum").asOpt[Int]
24+
.map(minimum => assertThat(number).isGreaterThanOrEqualTo(minimum)
25+
.otherwise[String](s"${path.toString} is smaller than required minimum value of $minimum")).toList ++
26+
propertyDefinition.\("exclusiveMaximum").asOpt[Int]
27+
.map(exclusiveMaximum => assertThat(number).isLessThan(exclusiveMaximum)
28+
.otherwise[String](s"${path.toString} exceeds exclusive maximum value of $exclusiveMaximum")).toList ++
29+
propertyDefinition.\("exclusiveMinimum").asOpt[Int]
30+
.map(exclusiveMinimum => assertThat(number).isGreaterThan(exclusiveMinimum)
31+
.otherwise[String](s"${path.toString} is smaller than required exclusive minimum value of $exclusiveMinimum")))
32+
.reduceOption[ComposableAssertionExp[JsValue,JsValue,JsValue]](_ ifTrue _)
33+
.getOrElse(SuccessfulAssertionExp())
34+
}
35+
}
36+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package jsonschemavalidation
2+
3+
import org.nulluncertainty.expression.{BooleanExp, ComposableBooleanExp, TrueBooleanExp, TrueExp}
4+
import play.api.libs.json.{JsArray, JsNull, JsObject, JsValue}
5+
6+
object JsObjectBooleanExpBuilder extends BooleanExpBuilder {
7+
8+
override def build(path: Path, objectDefinition: JsObject): ComposableBooleanExp[JsValue] = {
9+
10+
val allProperties: collection.Map[String, JsValue] =
11+
(objectDefinition \ "properties").as[JsObject].value
12+
13+
val requiredProperties: collection.Seq[String] =
14+
(objectDefinition \ "required").asOpt[JsArray]
15+
.map(_.value.map(_.as[String]))
16+
.getOrElse(Nil)
17+
18+
allProperties.keys.partition(requiredProperties.contains) match {
19+
case (requiredProperties, nonRequiredProperties) =>
20+
requiredProperties.foldLeft[ComposableBooleanExp[JsValue]](TrueBooleanExp()) {
21+
case (boolExp, requiredProperty) =>
22+
boolExp.and(
23+
BooleanExp({ js: JsValue => path.add(requiredProperty).valueIn(js).exists(_ != JsNull) }).isTrue
24+
.and(DelegatingJsBooleanExpBuilder.build(path.add(requiredProperty), allProperties(requiredProperty).as[JsObject])))
25+
}
26+
.and(
27+
nonRequiredProperties.foldLeft[ComposableBooleanExp[JsValue]](TrueBooleanExp()) {
28+
case (boolExp, nonRequiredProperty) =>
29+
boolExp.and(
30+
(context: JsValue) =>
31+
BooleanExp({ js: JsValue => path.add(nonRequiredProperty).valueIn(js).exists(_ != JsNull) }).isTrue
32+
.evaluate(context)
33+
.thenElse(
34+
DelegatingJsBooleanExpBuilder.build(path.add(nonRequiredProperty), allProperties(nonRequiredProperty).as[JsObject]).evaluate(context),
35+
TrueExp)
36+
)
37+
}
38+
)
39+
}
40+
}
41+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package jsonschemavalidation
2+
3+
import org.nulluncertainty.assertion.AssertionBuilder.assertThat
4+
import org.nulluncertainty.expression.{BooleanExp, ComposableAssertionExp, SuccessfulAssertionExp}
5+
import play.api.libs.json.{JsArray, JsNull, JsObject, JsValue}
6+
7+
object JsObjectValidationBuilder extends JsValidationBuilder {
8+
9+
override def build(path: Path, objectDefinition: JsObject): ComposableAssertionExp[JsValue,JsValue,JsValue] = {
10+
11+
val buildFrom: JsObject => ComposableAssertionExp[JsValue,JsValue,JsValue] = schema => {
12+
13+
val allProperties: collection.Map[String, JsValue] =
14+
(schema \ "properties").as[JsObject].value
15+
16+
val requiredProperties: collection.Seq[String] =
17+
(schema \ "required").asOpt[JsArray]
18+
.map(_.value.map(_.as[String]))
19+
.getOrElse(Nil)
20+
21+
allProperties.keys.partition(requiredProperties.contains) match {
22+
case (requiredProperties, nonRequiredProperties) =>
23+
requiredProperties.foldLeft[ComposableAssertionExp[JsValue,JsValue,JsValue]](SuccessfulAssertionExp()) {
24+
case (assertions, requiredProperty) =>
25+
assertions.and(
26+
assertThat({ js: JsValue => path.add(requiredProperty).valueIn(js).exists(_ != JsNull) }).isTrue
27+
.otherwise[String](s"Property ${path.add(requiredProperty).toString} is missing")
28+
.ifTrue(DelegatingJsValidationBuilder.build(path.add(requiredProperty), allProperties(requiredProperty).as[JsObject])))
29+
}
30+
.and(
31+
nonRequiredProperties.foldLeft[ComposableAssertionExp[JsValue,JsValue,JsValue]](SuccessfulAssertionExp()) {
32+
case (assertions, nonRequiredProperty) =>
33+
assertions.and(
34+
BooleanExp({ js: JsValue => path.add(nonRequiredProperty).valueIn(js).exists(_ != JsNull) }).isTrue
35+
.thenElse(
36+
(context: JsValue) => {
37+
DelegatingJsValidationBuilder.build(path.add(nonRequiredProperty), allProperties(nonRequiredProperty).as[JsObject]).evaluate(context)
38+
},
39+
SuccessfulAssertionExp()))
40+
}
41+
)
42+
}
43+
}
44+
45+
val maybeAllOf: Option[ComposableAssertionExp[JsValue,JsValue,JsValue]] =
46+
(objectDefinition \ "allOf").asOpt[JsArray].map { _.value.map { conditionalSubschemas =>
47+
DelegatingJsBooleanExpBuilder.build(path, (conditionalSubschemas \ "if").as[JsObject])
48+
.thenElse(
49+
buildFrom((conditionalSubschemas \ "then").as[JsObject]),
50+
(conditionalSubschemas \ "else").asOpt[JsObject].map(buildFrom).getOrElse(SuccessfulAssertionExp()))
51+
}.reduce[ComposableAssertionExp[JsValue,JsValue,JsValue]](_ and _)
52+
}
53+
54+
(maybeAllOf.toList :+ buildFrom(objectDefinition)).reduce(_ and _)
55+
}
56+
}

0 commit comments

Comments
 (0)