Skip to content

Commit

Permalink
Merge pull request #65 from hmrc/APIS-4689
Browse files Browse the repository at this point in the history
APIS-4689 - Added Validation for FieldDefinition
  • Loading branch information
anjumabbas5 authored Mar 31, 2020
2 parents 9aa7ec9 + 331344c commit 29a60fb
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 38 deletions.
39 changes: 35 additions & 4 deletions app/uk/gov/hmrc/apisubscriptionfields/model/JsonFormatters.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ package uk.gov.hmrc.apisubscriptionfields.model

import java.util.UUID

import cats.data.{NonEmptyList => NEL}
import cats.implicits._
import julienrf.json.derived
import julienrf.json.derived.TypeTagSetting
import play.api.libs.json._
import play.api.libs.json.Reads._
import play.api.libs.functional.syntax._
Expand All @@ -36,16 +40,43 @@ trait SharedJsonFormatters {

trait JsonFormatters extends SharedJsonFormatters {

implicit val FieldDefinitionTypeReads = Reads.enumNameReads(FieldDefinitionType)
object NonEmptyListOps {
def reads[T: Reads]: Reads[NEL[T]] =
Reads
.of[List[T]]
.collect(
JsonValidationError("expected a NonEmptyList but got an empty list")
) {
case head :: tail => NEL(head, tail)
}

def writes[T: Writes]: Writes[NEL[T]] =
Writes
.of[List[T]]
.contramap(_.toList)

def format[T: Format]: Format[NEL[T]] =
Format(reads, writes)
}

implicit val validationRuleFormat: OFormat[ValidationRule] = derived.withTypeTag.oformat(TypeTagSetting.ShortClassName)

implicit val nelValidationRuleFormat: Format[NEL[ValidationRule]] = NonEmptyListOps.format[ValidationRule]

implicit val ValidationJF = Json.format[Validation]

implicit val FieldDefinitionTypeReads = Reads.enumNameReads(FieldDefinitionType)
val fieldDefinitionReads: Reads[FieldDefinition] = (
(JsPath \ "name").read[String] and
(JsPath \ "description").read[String] and
((JsPath \ "hint").read[String] or Reads.pure("")) and
(JsPath \ "type").read[FieldDefinitionType] and
((JsPath \ "shortDescription").read[String] or Reads.pure(""))
)(FieldDefinition.apply _)
((JsPath \ "shortDescription").read[String] or Reads.pure("")) and
(JsPath \ "validation").readNullable[Validation]
)(FieldDefinition.apply _)

val fieldDefinitionWrites = Json.writes[FieldDefinition]

implicit val FieldDefinitionJF = Format(fieldDefinitionReads, fieldDefinitionWrites)

implicit val FieldsDefinitionRequestJF = Json.format[FieldsDefinitionRequest]
Expand All @@ -54,8 +85,8 @@ trait JsonFormatters extends SharedJsonFormatters {
implicit val FieldsDefinitionResponseJF = Json.format[FieldsDefinitionResponse]
implicit val BulkFieldsDefinitionsResponseJF = Json.format[BulkFieldsDefinitionsResponse]
implicit val SubscriptionFieldsResponseJF = Json.format[SubscriptionFieldsResponse]
implicit val BulkSubscriptionFieldsResponseJF = Json.format[BulkSubscriptionFieldsResponse]

implicit val BulkSubscriptionFieldsResponseJF = Json.format[BulkSubscriptionFieldsResponse]
}

object JsonFormatters extends JsonFormatters
10 changes: 9 additions & 1 deletion app/uk/gov/hmrc/apisubscriptionfields/model/Model.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package uk.gov.hmrc.apisubscriptionfields.model

import java.util.UUID

import cats.data.{NonEmptyList => NEL}
import uk.gov.hmrc.apisubscriptionfields.model.FieldDefinitionType.FieldDefinitionType

case class ClientId(value: String) extends AnyVal
Expand All @@ -28,6 +29,12 @@ case class ApiVersion(value: String) extends AnyVal

case class SubscriptionFieldsId(value: UUID) extends AnyVal

sealed trait ValidationRule

case class RegexValidationRule(regex: String) extends ValidationRule

case class Validation(errorMessage: String, rules: NEL[ValidationRule])

object FieldDefinitionType extends Enumeration {
type FieldDefinitionType = Value

Expand All @@ -36,4 +43,5 @@ object FieldDefinitionType extends Enumeration {
val STRING = Value("STRING")
}

case class FieldDefinition(name: String, description: String, hint: String = "", `type`: FieldDefinitionType, shortDescription: String)
case class FieldDefinition(name: String, description: String, hint: String = "", `type`: FieldDefinitionType,
shortDescription: String, validation: Option[Validation] = None)
9 changes: 7 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ import scala.language.postfixOps

val compile = Seq(
"uk.gov.hmrc" %% "bootstrap-play-26" % "1.4.0",
"uk.gov.hmrc" %% "simple-reactivemongo" % "7.22.0-play-26"
"uk.gov.hmrc" %% "simple-reactivemongo" % "7.22.0-play-26",
"org.julienrf" %% "play-json-derived-codecs" % "6.0.0",
"com.typesafe.play" %% "play-json" % "2.7.1",
"org.typelevel" %% "cats-core" % "2.0.0"
)

// we need to override the akka version for now as newer versions are not compatible with reactivemongo
Expand Down Expand Up @@ -56,7 +59,8 @@ val appName = "api-subscription-fields"

lazy val appDependencies: Seq[ModuleID] = compile ++ test()

resolvers ++= Seq(Resolver.bintrayRepo("hmrc", "releases"), Resolver.jcenterRepo)
resolvers ++= Seq(Resolver.bintrayRepo("hmrc", "releases"),
Resolver.jcenterRepo, Resolver.sonatypeRepo("releases"), Resolver.sonatypeRepo("snapshots"))

lazy val plugins: Seq[Plugins] = Seq(PlayScala, SbtAutoBuildPlugin, SbtGitVersioning, SbtDistributablesPlugin, SbtArtifactory)
lazy val playSettings: Seq[Setting[_]] = Seq.empty
Expand Down Expand Up @@ -85,6 +89,7 @@ lazy val microservice = Project(appName, file("."))
parallelExecution in Test := false
)
.settings(majorVersion := 0)
.settings(scalacOptions ++= Seq("-Ypartial-unification"))

lazy val acceptanceTestSettings =
inConfig(AcceptanceTest)(Defaults.testSettings) ++
Expand Down
11 changes: 8 additions & 3 deletions test/uk/gov/hmrc/apisubscriptionfields/TestData.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package uk.gov.hmrc.apisubscriptionfields

import java.util.UUID

import cats.data.NonEmptyList
import org.scalatest.TestData
import play.api.http.HeaderNames.{ACCEPT, CONTENT_TYPE}
import play.api.http.MimeTypes
import uk.gov.hmrc.apisubscriptionfields.model._
Expand Down Expand Up @@ -64,9 +66,12 @@ trait SubscriptionFieldsTestData extends TestData {
}

trait FieldsDefinitionTestData extends TestData {
final val FakeFieldDefinitionUrl = FieldDefinition("name1", "desc1", "hint1", FieldDefinitionType.URL, "short description")
final val FakeFieldDefinitionString = FieldDefinition("name2", "desc2", "hint2", FieldDefinitionType.STRING, "short description")
final val FakeFieldDefinitionSecureToken = FieldDefinition("name3", "desc3", "hint3", FieldDefinitionType.SECURE_TOKEN, "short description")
val FakeValidationRule: RegexValidationRule = RegexValidationRule("test regex")
val FakeValidation: Validation = Validation("error message", NonEmptyList.one(FakeValidationRule))
final val FakeFieldDefinitionUrl = FieldDefinition("name1", "desc1", "hint1", FieldDefinitionType.URL, "short description", Some(FakeValidation))
final val FakeFieldDefinitionUrlValidationEmpty = FieldDefinition("name1", "desc1", "hint1", FieldDefinitionType.URL, "short description", None)
final val FakeFieldDefinitionString = FieldDefinition("name2", "desc2", "hint2", FieldDefinitionType.STRING, "short description", Some(FakeValidation))
final val FakeFieldDefinitionSecureToken = FieldDefinition("name3", "desc3", "hint3", FieldDefinitionType.SECURE_TOKEN, "short description", Some(FakeValidation))
final val FakeFieldsDefinitions = Seq(FakeFieldDefinitionUrl, FakeFieldDefinitionString, FakeFieldDefinitionSecureToken)
final val FakeFieldsDefinition = FieldsDefinition(fakeRawContext, fakeRawVersion, FakeFieldsDefinitions)
final val FakeFieldsDefinitionResponse = FieldsDefinitionResponse(fakeRawContext, fakeRawVersion, FakeFieldsDefinition.fieldDefinitions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,39 @@ class FieldsDefinitionControllerGetSpec extends UnitSpec with FieldsDefinitionTe

private val responseJsonString =
"""{
| "apiContext": "hello",
| "apiVersion": "1.0",
| "fieldDefinitions": [
| {
| "name": "callback-url",
| "description": "Callback URL",
| "hint": "Description Hint",
| "type": "URL",
| "shortDescription": "short desc"
| },
| {
| "name": "token",
| "description": "Secure Token",
| "hint": "Description Hint",
| "type": "SecureToken",
| "shortDescription": ""
| }
| ]
| "apiContext": "hello",
| "apiVersion": "1.0",
| "fieldDefinitions": [
| {
| "name": "callback-url",
| "description": "Callback URL",
| "hint": "Description Hint",
| "type": "URL",
| "shortDescription": "short desc"
| },
| {
| "name": "token",
| "description": "Secure Token",
| "hint": "Description Hint",
| "type": "SecureToken",
| "shortDescription": "",
| "validation": {
| "errorMessage": "error message",
| "rules": [
| {
| "RegexValidationRule": {
| "regex": "test regex"
| }
| },
| {
| "RegexValidationRule": {
| "regex": "test regex"
| }
| }
| ]
| }
| }
| ]
|}""".stripMargin
private val responseJson = Json.parse(responseJsonString)
private val responseModel = responseJson.as[FieldsDefinitionResponse]
Expand All @@ -68,14 +83,34 @@ class FieldsDefinitionControllerGetSpec extends UnitSpec with FieldsDefinitionTe
| "description": "Callback URL",
| "hint": "Description Hint",
| "type": "URL",
| "shortDescription": "short desc"
| "shortDescription": "short desc",
| "validation": {
| "errorMessage": "",
| "rules": [
| {
| "RegexValidationRule": {
| "regex": "test regex"
| }
| }
| ]
| }
| },
| {
| "name": "token",
| "description": "Secure Token",
| "hint": "Description Hint",
| "type": "SecureToken",
| "shortDescription": ""
| "shortDescription": "",
| "validation": {
| "errorMessage": "",
| "rules": [
| {
| "RegexValidationRule": {
| "regex": "test regex"
| }
| }
| ]
| }
| }
| ]
| },
Expand All @@ -88,7 +123,17 @@ class FieldsDefinitionControllerGetSpec extends UnitSpec with FieldsDefinitionTe
| "description": "where you live",
| "hint": "Description Hint",
| "type": "STRING",
| "shortDescription": ""
| "shortDescription": "",
| "validation": {
| "errorMessage": "",
| "rules": [
| {
| "RegexValidationRule": {
| "regex": "test regex"
| }
| }
| ]
| }
| },
| {
| "name": "number",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,18 @@ class JsonFormatterSpec extends WordSpec
private val bulkSubscriptionFieldsResponse = BulkSubscriptionFieldsResponse(Seq(subscriptionFieldsResponse))

private val fakeFieldsDefinitionResponse = FieldsDefinitionResponse(fakeRawContext, fakeRawVersion, Seq(FakeFieldDefinitionUrl))
private val fakeFieldsDefinitionResponseEmptyValidation = FieldsDefinitionResponse(fakeRawContext, fakeRawVersion, Seq(FakeFieldDefinitionUrlValidationEmpty))
private val bulkFieldsDefinitionResponse = BulkFieldsDefinitionsResponse(Seq(fakeFieldsDefinitionResponse))

private def objectAsJsonString[A](a:A)(implicit t: Writes[A]) = Json.asciiStringify(Json.toJson(a))

private val subscriptionFieldJson = s"""{"clientId":"$fakeRawClientId","apiContext":"$fakeRawContext","apiVersion":"$fakeRawVersion","fieldsId":"$FakeRawFieldsId","fields":{"f1":"v1"}}"""
private val fieldDefinitionJson =
s"""{"apiContext":"$fakeRawContext","apiVersion":"$fakeRawVersion","fieldDefinitions":[{"name":"name1","description":"desc1","hint":"hint1","type":"URL","shortDescription":"short description","validation":{"errorMessage":"error message","rules":[{"RegexValidationRule":{"regex":"test regex"}}]}}]}"""
private val fieldDefinitionEmptyValidationJson =
s"""{"apiContext":"$fakeRawContext","apiVersion":"$fakeRawVersion","fieldDefinitions":[{"name":"name1","description":"desc1","hint":"hint1","type":"URL","shortDescription":"short description"}]}"""


"SubscriptionFieldsResponse" should {
"marshal json" in {
objectAsJsonString(subscriptionFieldsResponse) shouldBe subscriptionFieldJson
Expand Down Expand Up @@ -73,12 +77,23 @@ class JsonFormatterSpec extends WordSpec
objectAsJsonString(fakeFieldsDefinitionResponse) shouldBe fieldDefinitionJson
}

"marshal json when Validation is empty" in {
objectAsJsonString(fakeFieldsDefinitionResponseEmptyValidation) shouldBe fieldDefinitionEmptyValidationJson
}

"unmarshal text" in {
Json.parse(fieldDefinitionJson).validate[FieldsDefinitionResponse] match {
case JsSuccess(r, _) => r shouldBe fakeFieldsDefinitionResponse
case JsError(e) => fail(s"Should have parsed json text but got $e")
}
}

"unmarshal text when Validation is empty" in {
Json.parse(fieldDefinitionEmptyValidationJson).validate[FieldsDefinitionResponse] match {
case JsSuccess(r, _) => r shouldBe fakeFieldsDefinitionResponseEmptyValidation
case JsError(e) => fail(s"Should have parsed json text but got $e")
}
}
}

"BulkFieldsDefinitionsResponse" should {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,33 @@

package uk.gov.hmrc.apisubscriptionfields.repository

import cats.data.NonEmptyList
import play.api.libs.json.{JsSuccess, Json}
import uk.gov.hmrc.apisubscriptionfields.model.{FieldDefinition, FieldDefinitionType, JsonFormatters}
import uk.gov.hmrc.apisubscriptionfields.model.{FieldDefinition, FieldDefinitionType, JsonFormatters, RegexValidationRule, Validation}
import uk.gov.hmrc.play.test.UnitSpec

class MongoFormattersSpec extends UnitSpec with JsonFormatters {
val validationRule = RegexValidationRule("test regex")
final val validation = Validation("error message", NonEmptyList.one(validationRule))
"Field definition formatter" should {
"Correctly unmarshall a JSON field definition with all the necessary fields" in {
val fieldDefinition = FieldDefinition("name", "description", "hint", FieldDefinitionType.STRING, "short description")
Json.fromJson[FieldDefinition](Json.parse("""{ "name" : "name", "description" : "description", "hint": "hint", "type" : "STRING", "shortDescription" : "short description"}""")) shouldBe JsSuccess(fieldDefinition)
val fieldDefinition = FieldDefinition("name", "description", "hint", FieldDefinitionType.STRING, "short description", Some(validation))
Json.fromJson[FieldDefinition](Json.parse("""{ "name" : "name", "description" : "description", "hint": "hint", "type" : "STRING", "shortDescription" : "short description","validation":{"errorMessage":"error message","rules":[{"RegexValidationRule":{"regex":"test regex"}}]}}""")) shouldBe JsSuccess(fieldDefinition)
}

"Correctly unmarshall a JSON field definition without the hint field" in {
val fieldDefinition = FieldDefinition("name", "description", "", FieldDefinitionType.STRING, "short description")
Json.fromJson[FieldDefinition](Json.parse("""{ "name" : "name", "description" : "description", "type" : "STRING", "shortDescription" : "short description"}""")) shouldBe JsSuccess(fieldDefinition)
val fieldDefinition = FieldDefinition("name", "description", "", FieldDefinitionType.STRING, "short description", Some(validation))
Json.fromJson[FieldDefinition](Json.parse("""{ "name" : "name", "description" : "description", "type" : "STRING", "shortDescription" : "short description","validation":{"errorMessage":"error message","rules":[{"RegexValidationRule":{"regex":"test regex"}}]}}""")) shouldBe JsSuccess(fieldDefinition)
}

"Correctly unmarshall a JSON field definition without the shortDescription field" in {
val fieldDefinition = FieldDefinition("name", "description", "hint", FieldDefinitionType.STRING, "")
Json.fromJson[FieldDefinition](Json.parse("""{ "name" : "name", "description" : "description", "hint": "hint", "type" : "STRING"}""")) shouldBe JsSuccess(fieldDefinition)
val fieldDefinition = FieldDefinition("name", "description", "hint", FieldDefinitionType.STRING, "", Some(validation))
Json.fromJson[FieldDefinition](Json.parse("""{ "name" : "name", "description" : "description", "hint": "hint", "type" : "STRING","validation":{"errorMessage":"error message","rules":[{"RegexValidationRule":{"regex":"test regex"}}]}}""")) shouldBe JsSuccess(fieldDefinition)
}

"Correctly unmarshall a JSON field definition with empty validation field" in {
val fieldDefinition = FieldDefinition("name", "description", "hint", FieldDefinitionType.STRING, "short description", None)
Json.fromJson[FieldDefinition](Json.parse("""{ "name" : "name", "description" : "description", "hint": "hint", "type" : "STRING", "shortDescription" : "short description"}""")) shouldBe JsSuccess(fieldDefinition)
}
}
}

0 comments on commit 29a60fb

Please sign in to comment.