Skip to content
This repository has been archived by the owner on Dec 21, 2019. It is now read-only.

Commit

Permalink
Merge pull request #81 from edgarmueller/master
Browse files Browse the repository at this point in the history
Support for i18n and customization of error messages
  • Loading branch information
edgarmueller committed Dec 2, 2016
2 parents d1ded69 + a52d6ed commit 566b61a
Show file tree
Hide file tree
Showing 29 changed files with 305 additions and 205 deletions.
5 changes: 4 additions & 1 deletion project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ object Version {
final val scalaz = "7.2.0"
final val specs2 = "3.7.2"
final val guava = "19.0"
final val i18n = "1.0.0"
}

object Library {
Expand All @@ -17,6 +18,7 @@ object Library {
final val playJson = "com.typesafe.play" %% "play-json" % Version.play
final val playTest = "com.typesafe.play" %% "play-specs2" % Version.play % "test"
final val specs2 = "org.specs2" %% "specs2-core" % Version.specs2 % "test"
final val i18n = "com.osinka.i18n" %% "scala-i18n" % Version.i18n
}

object Dependencies {
Expand All @@ -27,7 +29,8 @@ object Dependencies {
playJson,
scalaz,
specs2,
guava
guava,
i18n
)
}

Expand Down
39 changes: 39 additions & 0 deletions src/main/resources/messages.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
obj.missing.prop.dep = Missing property dependency {0}.
obj.max.props = Too many properties. {0} properties found, but only a maximum of {1} is allowed.
obj.min.props = Found {0} properties, but a minimum of {1} is required.
obj.additional.props = Additional properties are not allowed, but found properties {0}.
obj.required.prop = Property {0} missing.

arr.max = Too many items. {0} items found, but only a maximum of {1} is allowed.
arr.min = Found {0} items, but a minimum of {1} is required.
arr.dups = Found duplicates.
arr.out.of.bounds = Array index {0} out of bounds.
arr.invalid.index = Invalid array index {0}.

str.pattern = ''{0}'' does not match pattern {1}.
str.min.length = ''{0}'' does not match minimum length of {1}.
str.max.length = ''{0}'' exceeds maximum length of {1}.
str.format = ''{0}'' does not match format {1}.
str.unknown.format = Unknown format ''{0}''.

num.multiple.of = {0} is not a multiple of {1}.
num.max = {0} exceeds maximum value of {1}.
num.max.exclusive = {0} exceeds exclusive maximum value of {1}.
num.min = {0} is smaller than required minimum value of {1}.
num.min.exclusive = {0} is smaller that required exclusive minimum value of {1}.

any.not = Instance matches schema although it must not.
any.all = Instance does not match all schemas.
any.any = Instance does not match any of the schemas.
any.one.of.none = Instance does not match any schema.
any.one.of.many = Instance matches more than one schema.
any.enum = Instance is invalid enum value.

comp.no.schema = No schema applicable.

err.expected.type = Wrong type. Expected {0}, was {1}.
err.unresolved.ref = Could not resolve ref {0}.
err.prop.not.found = Could not find property {0}.
err.unvisited.ref.expected = Expected to find unvisited ref at {0}.
err.res.scope.id.empty = Resolution scope ID must not be empty.
err.parse.json = Could not parse JSON.
19 changes: 12 additions & 7 deletions src/main/scala/com/eclipsesource/schema/SchemaValidator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.eclipsesource.schema.internal.SchemaRefResolver._
import com.eclipsesource.schema.internal.refs.Ref
import com.eclipsesource.schema.internal.validators.DefaultFormats
import com.eclipsesource.schema.urlhandlers.UrlHandler
import com.osinka.i18n.Lang
import play.api.data.validation.ValidationError
import play.api.libs.json._

Expand All @@ -17,8 +18,12 @@ trait Customizations {
def formats: Map[String, SchemaStringFormat]
}

trait HasLang {
implicit val lang: Lang
}

trait CanValidate {
self: Customizations =>
self: Customizations with HasLang =>

/**
* Validate the given JsValue against the schema located at the given URL.
Expand All @@ -28,10 +33,9 @@ trait CanValidate {
* @return a JsResult holding the validation result
*/
def validate(schemaUrl: URL, input: => JsValue): JsResult[JsValue] = {

def buildContext(schema: SchemaType): SchemaResolutionContext = {
val id = schema.constraints.any.id.map(Ref)
new SchemaResolutionContext(refResolver,
SchemaResolutionContext(refResolver,
new SchemaResolutionScope(schema, id.orElse(Some(Ref(schemaUrl.toString)))),
formats = formats
)
Expand All @@ -52,7 +56,7 @@ trait CanValidate {
* @param input the value to be validated
* @return a JsResult holding the validation result
*/
def validate[A](schemaUrl: URL, input: => JsValue, reads: Reads[A]) : JsResult[A] = {
def validate[A](schemaUrl: URL, input: => JsValue, reads: Reads[A]): JsResult[A] = {
validate(schemaUrl, input).fold(
valid = readWith(reads),
invalid = errors => JsError(essentialErrorInfo(errors, Some(input)))
Expand Down Expand Up @@ -102,7 +106,7 @@ trait CanValidate {
*/
def validate(schema: SchemaType)(input: => JsValue): JsResult[JsValue] = {
val id = schema.constraints.any.id.map(Ref)
val context = new SchemaResolutionContext(
val context = SchemaResolutionContext(
refResolver,
new SchemaResolutionScope(schema, id),
formats = formats
Expand All @@ -121,7 +125,7 @@ trait CanValidate {
* @param input the value to be validated
* @return a JsResult holding the validation result
*/
def validate[A](schema: SchemaType, input: => JsValue, reads: Reads[A]) : JsResult[A] = {
def validate[A](schema: SchemaType, input: => JsValue, reads: Reads[A]): JsResult[A] = {
val result = validate(schema)(input)
result.fold(
valid = readWith(reads),
Expand Down Expand Up @@ -203,7 +207,8 @@ trait CanValidate {
*/
case class SchemaValidator(refResolver: SchemaRefResolver = new SchemaRefResolver,
formats: Map[String, SchemaStringFormat] = DefaultFormats.formats)
extends CanValidate with Customizations {
(implicit val lang: Lang = Lang.Default)
extends CanValidate with Customizations with HasLang {

/**
* Add a URLStreamHandler that is capable of handling absolute with a specific scheme.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.eclipsesource.schema.internal


import com.eclipsesource.schema._
import com.eclipsesource.schema.internal.refs._
import com.eclipsesource.schema.internal.validators.DefaultFormats
import com.osinka.i18n.{Lang, Messages}
import play.api.data.validation.ValidationError
import play.api.libs.json.{JsArray, JsString}

Expand All @@ -14,22 +14,25 @@ object SchemaRefResolver {
implicit val schemaRefInstance = new CanHaveRef[SchemaType] {

// TODO make inner method
private def resolveConstraint(schema: SchemaType, constraint: String): Either[ValidationError, SchemaType] = {
private def resolveConstraint(schema: SchemaType, constraint: String)
(implicit lang: Lang): Either[ValidationError, SchemaType] = {
schema.constraints.resolvePath(constraint).fold[Either[ValidationError, SchemaType]](
Left(ValidationError(s"Could not resolve $constraint"))
Left(ValidationError(Messages("err.unresolved.ref", constraint)))
)(schema => Right(schema))
}

private def findAttribute(maybeObj: Option[SchemaObject], prop: String): Option[SchemaType] =
maybeObj.flatMap(_.properties.collectFirst { case attr if attr.name == prop => attr.schemaType})

private def findSchemaAttribute(props: Seq[SchemaAttribute], propName: String): Either[ValidationError, SchemaType] = {
private def findSchemaAttribute(props: Seq[SchemaAttribute], propName: String)
(implicit lang: Lang): Either[ValidationError, SchemaType] = {
props.collectFirst {
case SchemaAttribute(name, s) if name == propName => s
}.toRight(ValidationError(s"Could not find property $propName"))
}.toRight(ValidationError(Messages("err.prop.not.found", propName)))
}

override def resolve(schema: SchemaType, fragment: String): Either[ValidationError, SchemaType] = {
override def resolve(schema: SchemaType, fragment: String)
(implicit lang: Lang = Lang.Default): Either[ValidationError, SchemaType] = {

schema match {

Expand Down Expand Up @@ -61,20 +64,20 @@ object SchemaRefResolver {
fragment match {
case Keywords.Array.Items => Right(tuple)
case idx if isValidIndex(idx) => Right(items(idx.toInt))
case other => resolveConstraint(tuple, fragment)
case _ => resolveConstraint(tuple, fragment)
}

case schemaValue@SchemaValue(value) => (value, fragment) match {
case SchemaValue(value) => (value, fragment) match {
case (arr: JsArray, index) if Try {
index.toInt
}.isSuccess =>
val idx = index.toInt
if (idx > 0 && idx < arr.value.size) {
Right(SchemaValue(arr.value(idx)))
} else {
Left(ValidationError(s"Array index $index out of bounds"))
Left(ValidationError(Messages("out.of.bounds", index)))
}
case other => Left(ValidationError(s"Invalid array index $fragment"))
case _ => Left(ValidationError(Messages("arr.invalid.index", fragment)))
}

case p: PrimitiveSchemaType => resolveConstraint(p, fragment)
Expand All @@ -98,8 +101,10 @@ object SchemaRefResolver {
case class SchemaResolutionContext(refResolver: SchemaRefResolver,
scope: SchemaResolutionScope,
formats: Map[String, SchemaStringFormat] = DefaultFormats.formats) extends GenResolutionContext[SchemaType] {

def updateScope(scopeUpdateFn: SchemaResolutionScope => SchemaResolutionScope): SchemaResolutionContext =
copy(scope = scopeUpdateFn(scope))

}
type SchemaResolutionScope = GenResolutionScope[SchemaType]
type SchemaRefResolver = GenRefResolver[SchemaType]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ object SchemaUtil {

def toJson(errors: Seq[(JsPath, Seq[ValidationError])]): JsArray = {
val emptyErrors = Json.arr()
errors.foldLeft(emptyErrors) { case (accumulatedErrors, (path, validationErrors)) =>
errors.foldLeft(emptyErrors) { case (accumulatedErrors, (_, validationErrors)) =>
val maybeError = validationErrors.foldLeft(None: Option[JsObject])((aggregatedError, err) => err.args.headOption match {
case Some(o@JsObject(fields)) =>
case Some(o@JsObject(_)) =>
Some(
aggregatedError.fold(
deepMerge(o, Json.obj("msgs" -> err.messages))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.eclipsesource.schema.internal.validation.VA
import play.api.data.validation.ValidationError
import play.api.libs.json._

import scala.language.reflectiveCalls
import scala.util.{Failure, Success, Try}
import scalaz.{ReaderWriterState, Semigroup}

Expand All @@ -25,7 +26,7 @@ package object internal {
implicit class EitherExtensions[A, B](either: Either[A, B]) {
def orElse(e: => Either[A, B]): Either[A, B] = either match {
case r@Right(_) => r
case l@Left(_) => e
case Left(_) => e
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,54 +1,55 @@
package com.eclipsesource.schema.internal.refs

import com.osinka.i18n.Lang
import play.api.data.validation.ValidationError


/**
* A typeclass that determines whether a value of the given
* type can contain references
*
* @tparam A the type that can contain references
* @tparam A the type of document that can contain references
*/
trait CanHaveRef[A] {

/**
* Resolve the fragment against the given value. A fragment
* is a single identifier like a property name.
* Resolve the fragment against the given document. A fragment
* is a single identifier, e.g. a property name.
*
* @param a the value
* @param a the document value
* @param fragment the fragment to be resolved
* @return a right-based Either containg the result
* @return a right-based Either containing the result
*/
def resolve(a: A, fragment: String): Either[ValidationError, A]
def resolve(a: A, fragment: String)(implicit lang: Lang): Either[ValidationError, A]

/**
* Whether the given value has an id field which can alter resolution scope.
* Whether the given document value has an id field which can alter resolution scope.
*
* @param a the instance to be checked
* @param a the document instance to be checked
* @return true, if the given instance has an id field, false otherwise
*/
def refinesScope(a: A): Boolean = findScopeRefinement(a).isDefined

/**
* Tries to find an id field which refines the resolution scope.
*
* @param a the instance to be checked
* @param a the document instance to be checked
* @return true, if the given instance has an id field, false otherwise
*/
def findScopeRefinement(a: A): Option[Ref]

/**
* Returns any anchors.
*
* @param a
* @return
* @param a a given document instance
* @return a Map mapping any found anchors to the respective sub-documents
*/
def anchorsOf(a: A): Map[Ref, A]

/**
* Tries to find a resolvable instance within the given value.
*
* @param a the value
* @param a the given document value
* @return an Option containing the field name and value, if any ref has been found
*/
def findRef(a: A): Option[Ref]
Expand Down

0 comments on commit 566b61a

Please sign in to comment.