diff --git a/src/main/scala/app/CreateRepositoryServlet.scala b/src/main/scala/app/CreateRepositoryServlet.scala index 0a3665c..61fe92a 100644 --- a/src/main/scala/app/CreateRepositoryServlet.scala +++ b/src/main/scala/app/CreateRepositoryServlet.scala @@ -65,7 +65,7 @@ form.validateAsJSON(params) } - val form = Form( + val form = mapping( "name" -> trim(label("Repository name", text(required, maxlength(40), repository))), "description" -> trim(label("Description" , text())) )(RepositoryCreationForm.apply) diff --git a/src/main/scala/util/Validations.scala b/src/main/scala/util/Validations.scala index 0019214..6204f2c 100644 --- a/src/main/scala/util/Validations.scala +++ b/src/main/scala/util/Validations.scala @@ -5,50 +5,88 @@ object Validations { - def withValidation[T](form: Form[T], params: Map[String, String])(action: T => Any): Any = { - form.validate(params).isEmpty match { - case true => action(form.create(params)) + /** + * Runs form validation before action. + * If there are validation error, action is not invoked and this method throws RuntimeException. + * + * @param mapping the mapping definition + * @param params the request parameters + * @param action the action + * @return the result of action + */ + def withValidation[T](mapping: MappingValueType[T], params: Map[String, String])(action: T => Any): Any = { + mapping.validate("form", params).isEmpty match { + case true => action(mapping.convert("form", params)) case false => throw new RuntimeException("Invalid Request") // TODO show error page? } } - def Form[T, P1](f1: (String, ValueType[P1]))(factory: (P1) => T): Form[T] = new Form[T]{ - def fields = Seq(f1) - def create(params: Map[String, String]) = factory(f1._2.convert(params(f1._1))) + ///////////////////////////////////////////////////////////////////////////////////////////// + // ValueTypes + + trait ValueType[T] { + def convert(name: String, params: Map[String, String]): T + def validate(name: String, params: Map[String, String]): Seq[(String, String)] } - def Form[T, P1, P2](f1: (String, ValueType[P1]), f2: (String, ValueType[P2]))(factory: (P1, P2) => T): Form[T] = new Form[T]{ - def fields = Seq(f1, f2) - def create(params: Map[String, String]) = factory(f1._2.convert(params(f1._1)), f2._2.convert(params(f2._1))) - } - - def Form[T, P1, P2, P3](f1: (String, ValueType[P1]), f2: (String, ValueType[P2]), f3: (String, ValueType[P3]))(factory: (P1, P2, P3) => T): Form[T] = new Form[T]{ - def fields = Seq(f1, f2) - def create(params: Map[String, String]) = factory(f1._2.convert(params(f1._1)), f2._2.convert(params(f2._1)), f3._2.convert(params(f3._1))) - } - - abstract class Form[T] { + /** + * The base class for the single field ValueTypes. + */ + abstract class SingleValueType[T](constraints: Constraint*) extends ValueType[T]{ - def fields: Seq[(String, ValueType[_])] + def convert(name: String, params: Map[String, String]): T = convert(params(name)) - def create(params: Map[String, String]): T + def convert(value: String): T - def validate(params: Map[String, String]): Map[String, String] = { - fields.map { case (name, valueType) => - valueType.validate(name, params(name)) match { - case Some(message) => Some(name, message) - case None => None + def validate(name: String, params: Map[String, String]): Seq[(String, String)] = validate(name, params(name)) + + def validate(name: String, value: String): Seq[(String, String)] = validaterec(name, value, Seq(constraints: _*)) + + @scala.annotation.tailrec + private def validaterec(name: String, value: String, constraints: Seq[Constraint]): Seq[(String, String)] = { + constraints match { + case (x :: rest) => x.validate(name, value) match { + case Some(message) => Seq(name -> message) + case None => validaterec(name, value, rest) } - }.flatten.toMap + case Nil => Nil + case x => println(x); Nil + } } + } + + /** + * The base class for the object field ValueTypes. + */ + abstract class MappingValueType[T] extends ValueType[T]{ + + def fields: Seq[(String, ValueType[_])] + + def validate(name: String, params: Map[String, String]): Seq[(String, String)] = + fields.map { case (name, valueType) => valueType.validate(name, params) }.flatten + def validateAsJSON(params: Map[String, String]): JObject = { - JObject(validate(params).map { case (key, value) => + JObject(validate("form", params).map { case (key, value) => JField(key, JString(value)) }.toList) } } + def text(constraints: Constraint*): SingleValueType[String] = new SingleValueType[String](constraints: _*){ + def convert(value: String): String = value + } + + def mapping[T, P1](f1: (String, ValueType[P1]))(factory: (P1) => T): MappingValueType[T] = new MappingValueType[T]{ + def fields = Seq(f1) + def convert(name: String, params: Map[String, String]) = factory(f1._2.convert(f1._1, params)) + } + + def mapping[T, P1, P2](f1: (String, ValueType[P1]), f2: (String, ValueType[P2]))(factory: (P1, P2) => T): MappingValueType[T] = new MappingValueType[T]{ + def fields = Seq(f1, f2) + def convert(name: String, params: Map[String, String]) = factory(f1._2.convert(f1._1, params), f2._2.convert(f2._1, params)) + } + ///////////////////////////////////////////////////////////////////////////////////////////// // Constraints @@ -57,8 +95,9 @@ } def required: Constraint = new Constraint(){ - def validate(name: String, value: String): Option[String] = + def validate(name: String, value: String): Option[String] = { if(value.isEmpty) Some("%s is required.".format(name)) else None + } } def required(message: String): Constraint = new Constraint(){ @@ -84,35 +123,17 @@ } ///////////////////////////////////////////////////////////////////////////////////////////// - // ValueTypes - - abstract class ValueType[T](constraints: Constraint*) { - - def convert(value: String): T - - def validate(name: String, value: String): Option[String] = { - constraints.map(_.validate(name, value)).flatten.headOption - } - } - - def text(constraints: Constraint*): ValueType[String] = new ValueType[String](constraints: _*){ - def convert(value: String): String = value - } - - ///////////////////////////////////////////////////////////////////////////////////////////// // ValueType wrappers to provide additional features. - def trim[T](valueType: ValueType[T]): ValueType[T] = new ValueType[T](){ + def trim[T](valueType: SingleValueType[T]): SingleValueType[T] = new SingleValueType[T](){ def convert(value: String): T = valueType.convert(value.trim) - override def validate(name: String, value: String): Option[String] = valueType.validate(name, value.trim) + override def validate(name: String, value: String): Seq[(String, String)] = valueType.validate(name, value.trim) } - /** - * - */ - def label[T](label: String, valueType: ValueType[T]): ValueType[T] = new ValueType[T](){ - def convert(value: String): T = valueType.convert(value.trim) - override def validate(name: String, value: String): Option[String] = valueType.validate(label, value.trim) + def label[T](label: String, valueType: SingleValueType[T]): SingleValueType[T] = new SingleValueType[T](){ + def convert(value: String): T = valueType.convert(value) + override def validate(name: String, value: String): Seq[(String, String)] = + valueType.validate(label, value).map { case (label, message) => name -> message } } } \ No newline at end of file