Skip to content

Commit

Permalink
Merge pull request #274 from pluralsight/enhancement/infer-optional-f…
Browse files Browse the repository at this point in the history
…ields-topicmetadatv2

Making createdDate parentSubjects and deprecated optional fields
  • Loading branch information
lewisjkl authored Mar 3, 2020
2 parents 3ec4916 + 19303bc commit f5e55bf
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import org.apache.avro.Schema
import spray.json.{
DefaultJsonProtocol,
DeserializationException,
JsNull,
JsObject,
JsString,
JsValue,
Expand Down Expand Up @@ -54,19 +55,7 @@ sealed trait TopicMetadataV2Parser
implicit object InstantFormat extends RootJsonFormat[Instant] {
override def write(obj: Instant): JsValue = JsString(obj.toString)

override def read(json: JsValue): Instant = json match {
case JsString(value) =>
Try(Instant.parse(value)).getOrElse(
throw DeserializationException(
CreatedDateNotSpecifiedAsISO8601(json).errorMessage
)
)
case _ =>
throwDeserializationError(
"createdDate",
"ISO-8601 DateString formatted YYYY-MM-DDThh:mm:ssZ"
)
}
override def read(json: JsValue): Instant = Instant.now()
}

implicit object ContactFormat
Expand Down Expand Up @@ -300,18 +289,12 @@ sealed trait TopicMetadataV2Parser
.getOrElse(throwDeserializationError("contact", "JsObject"))
)
)
val createdDate = toResult(
InstantFormat.read(
j.getFields("createdDate").headOption.getOrElse(JsString.empty)
)
)
val createdDate = toResult(InstantFormat.read(j))
val parentSubjects = toResult(
j.getFields("parentSubjects")
.headOption
.map(_.convertTo[List[Subject]])
.getOrElse(
throwDeserializationError("parentSubjects", "List of Subject")
)
.getOrElse(List())
)
val notes = toResult(
j.getFields("notes").headOption.map(_.convertTo[String])
Expand Down Expand Up @@ -343,8 +326,7 @@ sealed trait TopicMetadataV2Parser
json
.getFields(key)
.headOption
.map(_.convertTo[Boolean])
.getOrElse(throwDeserializationError(key, "Boolean"))
.exists(_.convertTo[Boolean])
}
}

Expand Down Expand Up @@ -374,12 +356,6 @@ final case class ExceptionThrownOnParseWithException(message: String)

object Errors {

final case class CreatedDateNotSpecifiedAsISO8601(value: JsValue) {

def errorMessage: String =
s"Field `createdDate` expected ISO-8601 DateString formatted YYYY-MM-DDThh:mm:ssZ, received ${value.compactPrint}."
}

def invalidPayloadProvided(actual: JsValue): String = {
import spray.json._
val expected =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package hydra.kafka.serializers

import java.time.{Instant, ZoneOffset}
import java.time.Instant

import cats.data.NonEmptyList
import hydra.core.marshallers._
Expand All @@ -10,6 +10,8 @@ import hydra.kafka.model._
import hydra.kafka.serializers.Errors._
import org.scalatest.{Matchers, WordSpec}

import scala.concurrent.duration._

class TopicMetadataV2ParserSpec extends WordSpec with Matchers {
import TopicMetadataV2Parser._
import spray.json._
Expand All @@ -32,23 +34,10 @@ class TopicMetadataV2ParserSpec extends WordSpec with Matchers {

"TopicMetadataV2Deserializer" must {

"parse a valid ISO-8601 date in Zulu time" in {
val instant = InstantFormat
.read(JsString("2020-01-20T12:34:56Z"))
.atOffset(ZoneOffset.UTC)
instant.getMonth.getValue shouldBe 1
instant.getDayOfMonth shouldBe 20
instant.getYear shouldBe 2020
instant.getHour shouldBe 12
instant.getMinute shouldBe 34
instant.getSecond shouldBe 56
}

"throw a Deserialization error with invalid date string" in {
val invalidJsString = JsString.empty
the[DeserializationException] thrownBy {
InstantFormat.read(invalidJsString)
} should have message CreatedDateNotSpecifiedAsISO8601(invalidJsString).errorMessage
"return instant.now" in {
InstantFormat
.read(JsNull)
.toEpochMilli shouldBe (Instant.now.toEpochMilli +- 10.seconds.toMillis)
}

"parse list of contact method with email and slack channel" in {
Expand Down Expand Up @@ -201,17 +190,17 @@ class TopicMetadataV2ParserSpec extends WordSpec with Matchers {
dataClassification,
email,
slackChannel,
createdDateString,
parentSubjects,
notes
) =
createJsValueOfTopicMetadataV2Request(
Subject.createValidated("Foo").get,
"#slack_channel",
"email@address.com",
"2020-01-20T12:34:56Z"
"email@address.com"
)()
TopicMetadataV2Format.read(jsonData) shouldBe
val tmv2 = TopicMetadataV2Format.read(jsonData)

tmv2 shouldBe
TopicMetadataV2Request(
subject,
Schemas(
Expand All @@ -222,12 +211,49 @@ class TopicMetadataV2ParserSpec extends WordSpec with Matchers {
deprecated,
dataClassification,
NonEmptyList(email, slackChannel :: Nil),
Instant.parse(createdDateString),
tmv2.createdDate,
parentSubjects,
notes
)
}

"parse a complete object with no optional fields and return a TopicMetadataV2Request" in {
val (
jsonData,
subject,
streamType,
_,
dataClassification,
email,
slackChannel,
_,
notes
) =
createJsValueOfTopicMetadataV2Request(
Subject.createValidated("Foo").get,
"#slack_channel",
"email@address.com",
allOptionalFieldsPresent = false
)()
val tmv2 = TopicMetadataV2Format.read(jsonData)

tmv2 shouldBe
TopicMetadataV2Request(
subject,
Schemas(
new SchemaFormat(isKey = true).read(validAvroSchema),
new SchemaFormat(isKey = false).read(validAvroSchema)
),
streamType,
deprecated = false,
dataClassification,
NonEmptyList(email, slackChannel :: Nil),
tmv2.createdDate,
parentSubjects = List(),
notes
)
}

"throw deserialization error with invalid payload" in {
the[DeserializationException] thrownBy {
TopicMetadataV2Format.read(JsString.empty)
Expand All @@ -246,11 +272,8 @@ class TopicMetadataV2ParserSpec extends WordSpec with Matchers {
error,
"Field `schemas`",
"Field `streamType`",
"Field `deprecated`",
"Field `dataClassification`",
"Field `contact`",
"Field `createdDate`",
"Field `parentSubjects`"
"Field `contact`"
)
)
}
Expand All @@ -261,14 +284,15 @@ class TopicMetadataV2ParserSpec extends WordSpec with Matchers {
subject: Subject,
slackChannel: String,
email: String,
createdDate: String
allOptionalFieldsPresent: Boolean = true
)(
streamType: StreamType = History,
deprecated: Boolean = false,
dataClassification: DataClassification = Public,
validAvroSchema: JsValue = validAvroSchema,
parentSubjects: List[Subject] = List(),
notes: Option[String] = None
notes: Option[String] = None,
createdDate: Instant = Instant.now()
): (
JsValue,
Subject,
Expand All @@ -277,7 +301,6 @@ class TopicMetadataV2ParserSpec extends WordSpec with Matchers {
DataClassification,
Email,
Slack,
String,
List[Subject],
Option[String]
) = {
Expand All @@ -289,14 +312,14 @@ class TopicMetadataV2ParserSpec extends WordSpec with Matchers {
| "value": ${validAvroSchema.compactPrint}
| },
| "streamType": "${streamType.toString}",
| "deprecated": $deprecated,
| "dataClassification":"${dataClassification.toString}",
| "contact": {
| "slackChannel": "$slackChannel",
| "email": "$email"
| },
| "createdDate": "$createdDate",
| "parentSubjects": ${parentSubjects.toJson.compactPrint}
| }
| ${if (allOptionalFieldsPresent) {
s""","parentSubjects": ${parentSubjects.toJson.compactPrint},"deprecated":$deprecated,"createdDate":"${createdDate.toString}""""
} else ""}
| ${if (notes.isDefined) s""","notes": "${notes.get}"""" else ""}}
|""".stripMargin.parseJson
(
Expand All @@ -307,7 +330,6 @@ class TopicMetadataV2ParserSpec extends WordSpec with Matchers {
dataClassification,
Email.create(email).get,
Slack.create(slackChannel).get,
createdDate,
parentSubjects,
notes
)
Expand Down Expand Up @@ -410,15 +432,15 @@ class TopicMetadataV2ParserSpec extends WordSpec with Matchers {
createJsValueOfTopicMetadataV2Request(
subject,
slack.channel.value,
email.address.value,
createdDate.toString
email.address.value
)(
streamType,
deprecated,
dataClassification,
validAvroSchema,
parentSubjects,
notes
notes,
createdDate
)._1
}

Expand Down

0 comments on commit f5e55bf

Please sign in to comment.