From f5086c2228c825d5709989ed8e6f7edb06f3e51e Mon Sep 17 00:00:00 2001 From: To-om Date: Mon, 19 Jun 2017 11:35:04 +0200 Subject: [PATCH] #16 Add custom fields attribute format --- app/org/elastic4play/models/Attributes.scala | 234 +++++++++++++----- .../models/CustomAttributeSpec.scala | 55 ++++ version.sbt | 2 +- 3 files changed, 223 insertions(+), 68 deletions(-) create mode 100644 test/org/elastic4play/models/CustomAttributeSpec.scala diff --git a/app/org/elastic4play/models/Attributes.scala b/app/org/elastic4play/models/Attributes.scala index 3953900..eff3317 100644 --- a/app/org/elastic4play/models/Attributes.scala +++ b/app/org/elastic4play/models/Attributes.scala @@ -7,45 +7,51 @@ import com.sksamuel.elastic4s.mappings.FieldType._ import com.sksamuel.elastic4s.mappings._ import org.elastic4play.JsonFormat.dateFormat import org.elastic4play._ +import play.api.Logger +import play.api.libs.json._ import org.elastic4play.controllers.JsonFormat.{ fileInputValueFormat, inputValueFormat } import org.elastic4play.controllers._ import org.elastic4play.models.JsonFormat.{ binaryFormats, multiFormat, optionFormat } import org.elastic4play.services.JsonFormat.attachmentFormat import org.elastic4play.services.{ Attachment, DBLists } import org.scalactic.Accumulation.convertGenTraversableOnceToValidatable -import org.scalactic._ -import play.api.Logger -import play.api.libs.json._ - -import scala.language.{ existentials, implicitConversions, postfixOps } import scala.reflect.ClassTag +import scala.language.postfixOps import scala.util.Try +import org.scalactic._ abstract class AttributeFormat[T](val name: String)(implicit val jsFormat: Format[T]) { def checkJson(subNames: Seq[String], value: JsValue): JsValue Or Every[AttributeError] + def checkJsonForCreation(subNames: Seq[String], value: JsValue): JsValue Or Every[AttributeError] = checkJson(subNames, value) + def checkJsonForUpdate(subNames: Seq[String], value: JsValue): JsValue Or Every[AttributeError] = checkJson(subNames, value) + def inputValueToJson(subNames: Seq[String], value: InputValue): JsValue Or Every[AttributeError] = fromInputValue(subNames, value).map(v ⇒ jsFormat.writes(v)) + def fromInputValue(subNames: Seq[String], value: InputValue): T Or Every[AttributeError] + def elasticType(attributeName: String): TypedFieldDefinition + + protected def formatError(value: InputValue) = Bad(One(InvalidFormatAttributeError("", name, value))) } object TextAttributeFormat extends AttributeFormat[String]("text") { override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = value match { case _: JsString if subNames.isEmpty ⇒ Good(value) - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, JsonInputValue(value)))) + case _ ⇒ formatError(JsonInputValue(value)) } override def fromInputValue(subNames: Seq[String], value: InputValue): String Or Every[AttributeError] = { if (subNames.nonEmpty) - Bad(One(InvalidFormatAttributeError("", name, value))) + formatError(value) else value match { case StringInputValue(Seq(v)) ⇒ Good(v) case JsonInputValue(JsString(v)) ⇒ Good(v) - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) + case _ ⇒ formatError(value) } } @@ -55,7 +61,7 @@ object TextAttributeFormat extends AttributeFormat[String]("text") { object StringAttributeFormat extends AttributeFormat[String]("string") { override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = value match { case _: JsString if subNames.isEmpty ⇒ Good(value) - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, JsonInputValue(value)))) + case _ ⇒ formatError(JsonInputValue(value)) } override def fromInputValue(subNames: Seq[String], value: InputValue): String Or Every[AttributeError] = TextAttributeFormat.fromInputValue(subNames, value) match { @@ -67,32 +73,32 @@ object StringAttributeFormat extends AttributeFormat[String]("string") { } object DateAttributeFormat extends AttributeFormat[Date]("date") { - def parse(d: String): Date = { + def parse(d: String): Option[Date] = { Try { val datePattern = "yyyyMMdd'T'HHmmssZ" // FIXME val df = new java.text.SimpleDateFormat(datePattern) df.setLenient(false) df.parse(d) - } getOrElse { + } orElse Try { new Date(d.toLong) - } + } toOption } override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = value match { - case JsString(v) if subNames.isEmpty ⇒ try { parse(v); Good(value) } catch { case _: Throwable ⇒ Bad(One(InvalidFormatAttributeError("", name, JsonInputValue(value)))) } + case JsString(v) if subNames.isEmpty ⇒ parse(v).map(_ ⇒ Good(value)).getOrElse(formatError(JsonInputValue(value))) case JsNumber(_) if subNames.isEmpty ⇒ Good(value) - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, JsonInputValue(value)))) + case _ ⇒ formatError(JsonInputValue(value)) } override def fromInputValue(subNames: Seq[String], value: InputValue): Date Or Every[AttributeError] = { if (subNames.nonEmpty) - Bad(One(InvalidFormatAttributeError("", name, value))) + formatError(value) else { value match { - case StringInputValue(Seq(v)) ⇒ try { Good(parse(v)) } catch { case _: Throwable ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) } - case JsonInputValue(JsString(v)) ⇒ try { Good(parse(v)) } catch { case _: Throwable ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) } - case JsonInputValue(JsNumber(v)) ⇒ try { Good(new Date(v.toLong)) } catch { case _: Throwable ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) } - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) + case StringInputValue(Seq(v)) ⇒ parse(v).map(Good(_)).getOrElse(formatError(value)) + case JsonInputValue(JsString(v)) ⇒ parse(v).map(Good(_)).getOrElse(formatError(value)) + case JsonInputValue(JsNumber(v)) ⇒ Good(new Date(v.toLong)) + case _ ⇒ formatError(value) } } } @@ -103,17 +109,22 @@ object DateAttributeFormat extends AttributeFormat[Date]("date") { object BooleanAttributeFormat extends AttributeFormat[Boolean]("boolean") { override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = value match { case _: JsBoolean if subNames.isEmpty ⇒ Good(value) - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, JsonInputValue(value)))) + case _ ⇒ formatError(JsonInputValue(value)) } override def fromInputValue(subNames: Seq[String], value: InputValue): Boolean Or Every[AttributeError] = { if (subNames.nonEmpty) - Bad(One(InvalidFormatAttributeError("", name, value))) + formatError(value) else value match { - case StringInputValue(Seq(v)) ⇒ try { Good(v.toBoolean) } catch { case _: Throwable ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) } + case StringInputValue(Seq(v)) ⇒ try { + Good(v.toBoolean) + } + catch { + case _: Throwable ⇒ formatError(value) + } case JsonInputValue(JsBoolean(v)) ⇒ Good(v) - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) + case _ ⇒ formatError(value) } } @@ -123,17 +134,22 @@ object BooleanAttributeFormat extends AttributeFormat[Boolean]("boolean") { object NumberAttributeFormat extends AttributeFormat[Long]("number") { override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = value match { case _: JsNumber if subNames.isEmpty ⇒ Good(value) - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, JsonInputValue(value)))) + case _ ⇒ formatError(JsonInputValue(value)) } override def fromInputValue(subNames: Seq[String], value: InputValue): Long Or Every[AttributeError] = { if (subNames.nonEmpty) - Bad(One(InvalidFormatAttributeError("", name, value))) + formatError(value) else value match { - case StringInputValue(Seq(v)) ⇒ try { Good(v.toLong) } catch { case _: Throwable ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) } + case StringInputValue(Seq(v)) ⇒ try { + Good(v.toLong) + } + catch { + case _: Throwable ⇒ formatError(value) + } case JsonInputValue(JsNumber(v)) ⇒ Good(v.longValue) - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) + case _ ⇒ formatError(value) } } @@ -144,18 +160,33 @@ case class EnumerationAttributeFormat[T <: Enumeration](enum: T)(implicit tag: C extends AttributeFormat[T#Value](s"enumeration") { override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = value match { - case JsString(v) if subNames.isEmpty ⇒ try { enum.withName(v); Good(value) } catch { case _: Throwable ⇒ Bad(One(InvalidFormatAttributeError("", name, JsonInputValue(value)))) } - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, JsonInputValue(value)))) + case JsString(v) if subNames.isEmpty ⇒ try { + enum.withName(v); Good(value) + } + catch { + case _: Throwable ⇒ formatError(JsonInputValue(value)) + } + case _ ⇒ formatError(JsonInputValue(value)) } override def fromInputValue(subNames: Seq[String], value: InputValue): T#Value Or Every[AttributeError] = { if (subNames.nonEmpty) - Bad(One(InvalidFormatAttributeError("", name, value))) + formatError(value) else value match { - case StringInputValue(Seq(v)) ⇒ try { Good(enum.withName(v)) } catch { case _: Throwable ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) } - case JsonInputValue(JsString(v)) ⇒ try { Good(enum.withName(v)) } catch { case _: Throwable ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) } - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) + case StringInputValue(Seq(v)) ⇒ try { + Good(enum.withName(v)) + } + catch { + case _: Throwable ⇒ formatError(value) + } + case JsonInputValue(JsString(v)) ⇒ try { + Good(enum.withName(v)) + } + catch { + case _: Throwable ⇒ formatError(value) + } + case _ ⇒ formatError(value) } } @@ -166,17 +197,17 @@ case class ListEnumeration(enumerationName: String)(dblists: DBLists) extends At def items: Set[String] = dblists("list_" + enumerationName).cachedItems.map(_.mapTo[String]).toSet //getItems[String].map(_.map(_._2).toSet) override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = value match { case JsString(v) if subNames.isEmpty && items.contains(v) ⇒ Good(value) - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, JsonInputValue(value)))) + case _ ⇒ formatError(JsonInputValue(value)) } override def fromInputValue(subNames: Seq[String], value: InputValue): String Or Every[AttributeError] = { if (subNames.nonEmpty) - Bad(One(InvalidFormatAttributeError("", name, value))) + formatError(value) else value match { case StringInputValue(Seq(v)) if items.contains(v) ⇒ Good(v) case JsonInputValue(JsString(v)) if items.contains(v) ⇒ Good(v) - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) + case _ ⇒ formatError(value) } } @@ -185,18 +216,33 @@ case class ListEnumeration(enumerationName: String)(dblists: DBLists) extends At object UUIDAttributeFormat extends AttributeFormat[UUID]("uuid") { override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = value match { - case JsString(v) if subNames.isEmpty ⇒ try { UUID.fromString(v); Good(value) } catch { case _: Throwable ⇒ Bad(One(InvalidFormatAttributeError("", name, JsonInputValue(value)))) } - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, JsonInputValue(value)))) + case JsString(v) if subNames.isEmpty ⇒ try { + UUID.fromString(v); Good(value) + } + catch { + case _: Throwable ⇒ formatError(JsonInputValue(value)) + } + case _ ⇒ formatError(JsonInputValue(value)) } override def fromInputValue(subNames: Seq[String], value: InputValue): UUID Or Every[AttributeError] = { if (subNames.nonEmpty) - Bad(One(InvalidFormatAttributeError("", name, value))) + formatError(value) else value match { - case StringInputValue(Seq(v)) ⇒ try { Good(UUID.fromString(v)) } catch { case _: Throwable ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) } - case JsonInputValue(JsString(v)) ⇒ try { Good(UUID.fromString(v)) } catch { case _: Throwable ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) } - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) + case StringInputValue(Seq(v)) ⇒ try { + Good(UUID.fromString(v)) + } + catch { + case _: Throwable ⇒ formatError(value) + } + case JsonInputValue(JsString(v)) ⇒ try { + Good(UUID.fromString(v)) + } + catch { + case _: Throwable ⇒ formatError(value) + } + case _ ⇒ formatError(value) } } @@ -208,17 +254,17 @@ object HashAttributeFormat extends AttributeFormat[String]("hash") { override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = value match { case JsString(v) if subNames.isEmpty && v.forall(c ⇒ validDigits.contains(c)) ⇒ Good(value) - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, JsonInputValue(value)))) + case _ ⇒ formatError(JsonInputValue(value)) } override def fromInputValue(subNames: Seq[String], value: InputValue): String Or Every[AttributeError] = { if (subNames.nonEmpty) - Bad(One(InvalidFormatAttributeError("", name, value))) + formatError(value) else value match { case StringInputValue(Seq(v)) if v.forall(c ⇒ validDigits.contains(c)) ⇒ Good(v.toLowerCase) case JsonInputValue(JsString(v)) if v.forall(c ⇒ validDigits.contains(c)) ⇒ Good(v.toLowerCase) - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) + case _ ⇒ formatError(value) } } @@ -231,24 +277,24 @@ object AttachmentAttributeFormat extends AttributeFormat[Attachment]("attachment if (subNames.isEmpty && validJson.isDefined) Good(value) else - Bad(One(InvalidFormatAttributeError("", name, JsonInputValue(value)))) + formatError(JsonInputValue(value)) } val forbiddenChar = Seq('/', '\n', '\r', '\t', '\u0000', '\f', '`', '?', '*', '\\', '<', '>', '|', '\"', ':', ';') override def inputValueToJson(subNames: Seq[String], value: InputValue): JsValue Or Every[AttributeError] = { if (subNames.nonEmpty) - Bad(One(InvalidFormatAttributeError("", name, value))) + formatError(value) else value match { case fiv: FileInputValue if fiv.name.intersect(forbiddenChar).isEmpty ⇒ Good(Json.toJson(fiv)(fileInputValueFormat)) case aiv: AttachmentInputValue ⇒ Good(Json.toJson(aiv.toAttachment)(jsFormat)) - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) + case _ ⇒ formatError(value) } } override def fromInputValue(subNames: Seq[String], value: InputValue): Attachment Or Every[AttributeError] = - Bad(One(InvalidFormatAttributeError("", name, value))) + formatError(value) override def elasticType(attributeName: String): NestedFieldDefinition = field(attributeName, NestedType) as ( field("name", StringType) index "not_analyzed", @@ -270,7 +316,7 @@ case class ObjectAttributeFormat(subAttributes: Seq[Attribute[_]]) extends Attri attr.validateForCreation((value \ attr.name).asOpt[JsValue]) } .map { _ ⇒ obj } - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, JsonInputValue(value)))) + case _ ⇒ formatError(JsonInputValue(value)) } } @@ -285,7 +331,7 @@ case class ObjectAttributeFormat(subAttributes: Seq[Attribute[_]]) extends Attri .getOrElse(Bad(One(UnknownAttributeError(_name, v)))) } .map { _ ⇒ obj } - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, JsonInputValue(value)))) + case _ ⇒ formatError(JsonInputValue(value)) } } @@ -317,7 +363,7 @@ case class ObjectAttributeFormat(subAttributes: Seq[Attribute[_]]) extends Attri .getOrElse(Bad(One(UnknownAttributeError(_name, Json.toJson(value))))) } .map { _ ⇒ v } - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) + case _ ⇒ formatError(value) } } } @@ -326,9 +372,9 @@ case class ObjectAttributeFormat(subAttributes: Seq[Attribute[_]]) extends Attri } object BinaryAttributeFormat extends AttributeFormat[Array[Byte]]("binary")(binaryFormats) { - override def checkJson(subNames: Seq[String], value: JsValue) = Bad(One(InvalidFormatAttributeError("", name, JsonInputValue(value)))) + override def checkJson(subNames: Seq[String], value: JsValue): Bad[JsValue, One[InvalidFormatAttributeError]] = formatError(JsonInputValue(value)) - override def fromInputValue(subNames: Seq[String], value: InputValue): Array[Byte] Or Every[AttributeError] = sys.error("not supported") + override def fromInputValue(subNames: Seq[String], value: InputValue): Array[Byte] Or Every[AttributeError] = formatError(value) override def elasticType(attributeName: String): BinaryFieldDefinition = field(attributeName, BinaryType) } @@ -344,10 +390,10 @@ object MetricsAttributeFormat extends AttributeFormat[JsValue]("metrics") { .validatedBy { case (_, _: JsNumber) ⇒ Good(()) case (_, JsNull) ⇒ Good(()) - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) + case _ ⇒ formatError(value) } .map(_ ⇒ v) - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) + case _ ⇒ formatError(value) } } else { @@ -358,32 +404,87 @@ object MetricsAttributeFormat extends AttributeFormat[JsValue]("metrics") { override def elasticType(attributeName: String): ObjectFieldDefinition = field(attributeName, ObjectType).as(field("_default_", LongType)) } +object CustomAttributeFormat extends AttributeFormat[JsValue]("custom") { + override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, Every[AttributeError]] = fromInputValue(subNames, JsonInputValue(value)) + + override def checkJsonForCreation(subNames: Seq[String], value: JsValue): Or[JsValue, Every[AttributeError]] = { + if (subNames.isEmpty && objectIsValid(value)) Good(value) + else formatError(JsonInputValue(value)) + } + + private def objectIsValid(v: JsValue) = v match { + case JsObject(fields) ⇒ fields.values.forall(objectFieldsIsValid) + case _ ⇒ false + } + + private def objectFieldsIsValid(v: JsValue) = v match { + case JsObject(fields) ⇒ fields.forall(fieldIsValid) + case _ ⇒ false + } + + private def fieldIsValid(f: (String, JsValue)): Boolean = f match { + case ("number", _: JsNumber | JsNull) ⇒ true + case ("string", _: JsString | JsNull) ⇒ true + case ("date", JsString(d)) ⇒ DateAttributeFormat.parse(d).isDefined + case ("date", JsNull) ⇒ true + case ("date", _: JsNumber | JsNull) ⇒ true + case ("boolean", _: JsBoolean | JsNull) ⇒ true + case ("order", _: JsNumber | JsNull) ⇒ true + case _ ⇒ false + } + + override def checkJsonForUpdate(subNames: Seq[String], value: JsValue): Or[JsValue, Every[AttributeError]] = { + (subNames, value) match { + case (Nil, _) ⇒ checkJsonForCreation(subNames, value) + case (Seq(_), v) ⇒ if (objectFieldsIsValid(v)) Good(value) else formatError(JsonInputValue(value)) + case (Seq(_, tpe), v) ⇒ if (fieldIsValid(tpe → v)) Good(value) else formatError(JsonInputValue(value)) + case _ ⇒ formatError(JsonInputValue(value)) + } + } + + override def fromInputValue(subNames: Seq[String], value: InputValue): JsValue Or Every[AttributeError] = { + value match { + case JsonInputValue(v) ⇒ checkJsonForUpdate(subNames, v) + case _ ⇒ formatError(value) + } + } + + override def elasticType(attributeName: String): ObjectFieldDefinition = + field(attributeName, ObjectType) as + field("_default_", ObjectType).as( + field("number", LongType), + field("string", StringType) index "not_analyzed", + field("date", DateType) format "epoch_millis||basic_date_time_no_millis", + field("boolean", BooleanType), + field("order", LongType)) +} + case class MultiAttributeFormat[T](attributeFormat: AttributeFormat[T]) extends AttributeFormat[Seq[T]]("multi-" + attributeFormat.name)(multiFormat(attributeFormat.jsFormat)) { // {//}(Format(Reads.seq(attributeFormat.jsFormat), Writes.seq(attributeFormat.jsFormat))) { override def checkJsonForCreation(subNames: Seq[String], value: JsValue): Or[JsArray, Every[AttributeError]] = value match { case JsArray(values) if subNames.isEmpty ⇒ values.validatedBy(v ⇒ attributeFormat.checkJsonForCreation(Nil, v)).map(JsArray) - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, JsonInputValue(value)))) + case _ ⇒ formatError(JsonInputValue(value)) } override def checkJsonForUpdate(subNames: Seq[String], value: JsValue): Or[JsArray, Every[AttributeError]] = value match { case JsArray(values) if subNames.isEmpty ⇒ values.validatedBy(v ⇒ attributeFormat.checkJsonForUpdate(Nil, v)).map(JsArray) - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, JsonInputValue(value)))) + case _ ⇒ formatError(JsonInputValue(value)) } override def checkJson(subNames: Seq[String], value: JsValue): Or[JsArray, Every[AttributeError]] = value match { case JsArray(values) if subNames.isEmpty ⇒ values.validatedBy(v ⇒ attributeFormat.checkJsonForUpdate(Nil, v)).map(JsArray) - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, JsonInputValue(value)))) + case _ ⇒ formatError(JsonInputValue(value)) } override def inputValueToJson(subNames: Seq[String], value: InputValue): JsValue Or Every[AttributeError] = value match { case JsonInputValue(JsArray(xs)) ⇒ xs.map(x ⇒ JsonInputValue(x)).validatedBy(i ⇒ attributeFormat.inputValueToJson(subNames, i)).map(JsArray) case StringInputValue(xs) ⇒ xs.filterNot(_.isEmpty).map(x ⇒ StringInputValue(x :: Nil)).validatedBy(i ⇒ attributeFormat.inputValueToJson(subNames, i)).map(JsArray) - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) + case _ ⇒ formatError(value) } override def fromInputValue(subNames: Seq[String], value: InputValue): Seq[T] Or Every[AttributeError] = value match { case JsonInputValue(JsArray(xs)) ⇒ xs.map(JsonInputValue).validatedBy(i ⇒ attributeFormat.fromInputValue(subNames, i)) case StringInputValue(xs) ⇒ xs.filterNot(_.isEmpty).map(x ⇒ StringInputValue(x :: Nil)).validatedBy(i ⇒ attributeFormat.fromInputValue(subNames, i)) - case _ ⇒ Bad(One(InvalidFormatAttributeError("", name, value))) + case _ ⇒ formatError(value) } override def elasticType(attributeName: String): TypedFieldDefinition = attributeFormat.elasticType(attributeName) @@ -414,17 +515,16 @@ object AttributeFormat { val stringFmt = StringAttributeFormat val booleanFmt = BooleanAttributeFormat val numberFmt = NumberAttributeFormat - - def enumFmt[T <: Enumeration](e: T)(implicit tag: ClassTag[T], format: Format[T#Value]): EnumerationAttributeFormat[T] = EnumerationAttributeFormat[T](e) - + val attachmentFmt = AttachmentAttributeFormat + val metricsFmt = MetricsAttributeFormat + val customFields = CustomAttributeFormat val uuidFmt = UUIDAttributeFormat val hashFmt = HashAttributeFormat val binaryFmt = BinaryAttributeFormat - def listEnumFmt(enumerationName: String)(dblists: DBLists): ListEnumeration = ListEnumeration(enumerationName)(dblists) + def enumFmt[T <: Enumeration](e: T)(implicit tag: ClassTag[T], format: Format[T#Value]): EnumerationAttributeFormat[T] = EnumerationAttributeFormat[T](e) - val attachmentFmt = AttachmentAttributeFormat - val metricsFmt = MetricsAttributeFormat + def listEnumFmt(enumerationName: String)(dblists: DBLists): ListEnumeration = ListEnumeration(enumerationName)(dblists) def objectFmt(subAttributes: Seq[Attribute[_]]) = ObjectAttributeFormat(subAttributes) } diff --git a/test/org/elastic4play/models/CustomAttributeSpec.scala b/test/org/elastic4play/models/CustomAttributeSpec.scala new file mode 100644 index 0000000..d38afd0 --- /dev/null +++ b/test/org/elastic4play/models/CustomAttributeSpec.scala @@ -0,0 +1,55 @@ +package org.elastic4play.models + +import org.junit.runner.RunWith +import org.scalactic.Good +import org.specs2.mock.Mockito +import org.specs2.runner.JUnitRunner +import play.api.libs.json.{JsNumber, Json} +import play.api.test.PlaySpecification + +@RunWith(classOf[JUnitRunner]) +class CustomAttributeSpec extends PlaySpecification with Mockito { + "a custom fields attribute" should { + "accept valid JSON object" in { + val js = Json.obj( + "field1" -> Json.obj( + "number" -> 12 + ), + "field2" -> Json.obj( + "string" -> "plop" + ), + "field3" -> Json.obj( + "boolean" -> true + ) + ) + CustomAttributeFormat.checkJsonForCreation(Nil, js) must_=== Good(js) + } + + "refuse invalid JSON object" in { + val js = Json.obj( + "field1" -> Json.obj( + "number" -> "str" + ), + "field2" -> Json.obj( + "string" -> 12 + ), + "field3" -> Json.obj( + "boolean" -> 45 + ) + ) + val result = CustomAttributeFormat.checkJsonForCreation(Nil, js) + result.isBad must_=== true + } + + "accept update a single field" in { + val js = Json.obj("number" -> 14) + CustomAttributeFormat.checkJsonForUpdate(Seq("field-name"), js) must_=== Good(js) + } + + "accept update a single value" in { + val js = JsNumber(15) + CustomAttributeFormat.checkJsonForUpdate(Seq("field-name", "number"), js) must_=== Good(js) + } + + } +} diff --git a/version.sbt b/version.sbt index 02c303a..ef3435e 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -version := "1.1.6-SNAPSHOT" +version := "1.2.0-SNAPSHOT"