Skip to content

Commit

Permalink
fix when field is defined with a default value (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
austek authored Jan 14, 2024
1 parent fa262a8 commit 37b4c1d
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import au.com.dius.pact.core.model.matchingrules.{MatchingRule, MatchingRuleCate
import com.github.austek.pact.RuleParser.parseRules
import com.github.austek.plugin.avro.AvroPluginConstants.MatchingRuleCategoryName
import com.github.austek.plugin.avro.error.*
import com.github.austek.plugin.avro.utils.StringUtils._
import com.github.austek.plugin.avro.utils.StringUtils.*
import com.google.protobuf.ByteString
import com.google.protobuf.struct.Value
import com.google.protobuf.struct.Value.Kind.*
import com.typesafe.scalalogging.StrictLogging
import org.apache.avro.Schema
import org.apache.avro.{JsonProperties, Schema}
import org.apache.avro.Schema.Type.*
import org.apache.avro.generic.*
import org.apache.avro.io.EncoderFactory
Expand Down Expand Up @@ -96,7 +96,8 @@ object Avro {
rules: Seq[MatchingRule]
): Either[PluginError[_], AvroValue] = {
fieldValue match {
case value: String => fromString(path, fieldName, schemaType, value, rules)
case value: String => fromString(path, fieldName, schemaType, value, rules)
case _: JsonProperties.Null => Right(AvroNull(path, fieldName))
case _ =>
(schemaType match {
case BOOLEAN => Try(AvroBoolean(path, fieldName, fieldValue.asInstanceOf[Boolean], rules)).toEither
Expand Down Expand Up @@ -376,9 +377,9 @@ object Avro {
schema.getFields.asScala.toSeq
.map { schemaField =>
val fieldName = AvroFieldName(schemaField.name())
configFields.get(schemaField.name()) match {
case Some(configValue) => configuredField(rootPath, schemaField, fieldName, configValue)
case None => unConfiguredField(rootPath, schemaField, fieldName)
schemaField.schema().getType match {
case UNION => handleUnionField(rootPath, fieldName, schemaField, configFields.get(schemaField.name()))
case _ => handleField(rootPath, fieldName, schemaField, configFields.get(schemaField.name()))
}
}
.partitionMap(identity) match {
Expand All @@ -387,40 +388,30 @@ object Avro {
}
}

private def configuredField(
rootPath: PactFieldPath,
schemaField: Schema.Field,
fieldName: AvroFieldName,
configValue: Value
): Either[Seq[PluginError[_]], AvroValue] = {
schemaField.schema().getType match {
case STRING | INT | LONG | FLOAT | DOUBLE | BOOLEAN | ENUM | FIXED | BYTES | NULL =>
AvroValue(rootPath, fieldName, schemaField.schema(), configValue)
case RECORD => AvroRecord(rootPath :+ fieldName, fieldName, schemaField.schema(), configValue.getStructValue.fields)
case ARRAY => AvroArray(rootPath, fieldName, schemaField.schema(), configValue)
case MAP => AvroMap(rootPath, fieldName, schemaField.schema(), configValue)
case UNION => handleAvroUnion(rootPath, schemaField, fieldName, configValue)
}
}

private def handleAvroUnion(rootPath: PactFieldPath, schemaField: Schema.Field, fieldName: AvroFieldName, configValue: Value) = {
private def handleUnionField(rootPath: PactFieldPath, fieldName: AvroFieldName, schemaField: Schema.Field, mayBeConfigValue: Option[Value]) = {
val subTypes = schemaField.schema().getTypes.asScala
if (subTypes.size == 2 && subTypes.exists(_.getType == NULL)) {
subTypes.filterNot(_.getType == NULL).headOption match {
case Some(schema) => handleNullableField(rootPath, fieldName, configValue, schema)
case None => Left(Seq(PluginErrorException(FieldInvalidSchemaException(fieldName, configValue))))
(mayBeConfigValue, subTypes.filterNot(_.getType == NULL).headOption) match {
case (Some(configValue), Some(schema)) => handleConfiguredField(rootPath, fieldName, schema, configValue)
case (None, Some(schema)) => handleDefaultValue(rootPath, fieldName, schemaField, schema)
case (_, _) => Left(Seq(PluginErrorException(FieldInvalidSchemaException(fieldName, mayBeConfigValue))))
}
} else {
Left(Seq(PluginErrorException(FieldNotNullableException(fieldName, configValue))))
Left(Seq(PluginErrorException(FieldNotNullableException(fieldName, mayBeConfigValue))))
}
}

private def handleNullableField(
private def handleField(
rootPath: PactFieldPath,
fieldName: AvroFieldName,
configValue: Value,
schema: Schema
): Either[Seq[PluginError[_]], AvroValue] = {
schemaField: Schema.Field,
maybeConfigValue: Option[Value]
): Either[Seq[PluginError[_]], AvroValue] =
maybeConfigValue match
case Some(configValue) => handleConfiguredField(rootPath, fieldName, schemaField.schema(), configValue)
case None => handleNoneConfiguredField(rootPath, fieldName, schemaField)

private def handleConfiguredField(rootPath: PactFieldPath, fieldName: AvroFieldName, schema: Schema, configValue: Value) = {
schema.getType match {
case STRING | INT | LONG | FLOAT | DOUBLE | BOOLEAN | ENUM | FIXED | BYTES | NULL => AvroValue(rootPath, fieldName, schema, configValue)
case RECORD => AvroRecord(rootPath :+ fieldName, fieldName, schema, configValue.getStructValue.fields)
Expand All @@ -430,14 +421,29 @@ object Avro {
}
}

private def unConfiguredField(rootPath: PactFieldPath, schemaField: Schema.Field, fieldName: AvroFieldName): Either[Seq[PluginError[_]], AvroValue] = {
private def handleNoneConfiguredField(
rootPath: PactFieldPath,
fieldName: AvroFieldName,
schemaField: Schema.Field
): Either[Seq[PluginError[_]], AvroValue] = {
if (schemaField.hasDefaultValue) {
AvroValue(rootPath :+ fieldName, fieldName, schemaField.schema().getType, schemaField.defaultVal(), Seq.empty).left.map(e => Seq(e))
} else if (schemaField.schema().getType == UNION && schemaField.schema().getTypes.asScala.exists(_.getType == NULL)) {
Right(AvroNull(rootPath :+ fieldName, fieldName))
handleDefaultValue(rootPath, fieldName, schemaField, schemaField.schema())
} else {
Left(Seq(PluginErrorException(new Exception(s"Couldn't find configuration for field: ${schemaField.name()}"))))
}
}

private def handleDefaultValue(
rootPath: PactFieldPath,
fieldName: AvroFieldName,
schemaField: Schema.Field,
schema: Schema
): Either[Seq[PluginError[_]], AvroValue] =
Option(schemaField.defaultVal()) match
case Some(value) => AvroValue(rootPath :+ fieldName, fieldName, schema.getType, value, Seq.empty).left.map(e => Seq(e))
case None => handleNullField(rootPath, fieldName)

private def handleNullField(rootPath: PactFieldPath, fieldName: AvroFieldName) =
Right(AvroNull(rootPath :+ fieldName, fieldName))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.github.austek.plugin.avro.TestSchemas.*
import com.github.austek.plugin.avro.utils.MatchingRuleCategoryImplicits.*
import com.google.protobuf.struct.Value.Kind.*
import com.google.protobuf.struct.{ListValue as StructListValue, Struct, Value}
import org.apache.avro.Schema.Type.NULL
import org.apache.avro.generic.{GenericData, GenericRecord}
import org.scalatest.EitherValues
import org.scalatest.matchers.should.Matchers
Expand Down Expand Up @@ -50,7 +51,7 @@ class AvroRecordComplexTypesTest extends AnyWordSpecLike with Matchers with Eith
genericRecord.get("color") shouldBe new GenericData.EnumSymbol(schema, "UNKNOWN")
}
"returns empty matching rules using JsonPath" in {
avroRecord.matchingRules.getRules("$.color") shouldBe empty
avroRecord.matchingRules shouldBe empty
}
}
}
Expand Down Expand Up @@ -86,7 +87,7 @@ class AvroRecordComplexTypesTest extends AnyWordSpecLike with Matchers with Eith
genericRecord.get("md5") shouldBe new GenericData.Fixed(schema.getField("md5").schema(), "\\u0000".getBytes)
}
"returns empty matching rules using JsonPath" in {
avroRecord.matchingRules.getRules("$.md5") shouldBe empty
avroRecord.matchingRules shouldBe empty
}
}
}
Expand Down Expand Up @@ -250,4 +251,83 @@ class AvroRecordComplexTypesTest extends AnyWordSpecLike with Matchers with Eith
}
}
}

"Optional Complex field" when {
val schema = schemaWithField("""{
| "name": "address",
| "type": [ "null",
| {
| "name": "Address",
| "type": "record",
| "fields": [
| { "name": "street", "type": "string" }
| ]
| }
| ]
|}""".stripMargin)
"value provided" should provide {
val pactConfiguration = Map("address" -> Value(StructValue(Struct(Map("street" -> Value(StringValue("matching(equalTo, 'first street')")))))))
val avroRecord = AvroRecord(schema, pactConfiguration).value
val addressSchema = schema.getField("address").schema().getTypes.asScala.filterNot(_.getType == NULL).head
val addressRecord = new GenericData.Record(addressSchema)
addressRecord.put("street", "first street")

"a method," which {
"returns GenericRecord with field" in {
val genericRecord = avroRecord.toGenericRecord(schema)
genericRecord.get("address") shouldBe addressRecord
}
"returns matching rules using JsonPath" in {
avroRecord.matchingRules should have size 1
avroRecord.matchingRules.getRules("$.address.street") shouldBe List(EqualsMatcher.INSTANCE)
}
}
}

"value not provided but has default" should provide {
val schema = schemaWithField("""{
| "name": "address",
| "type": [ "null",
| {
| "name": "Address",
| "type": "record",
| "fields": [
| { "name": "street", "type": "string", "default": "first street" }
| ]
| }
| ],
| "default": null
|}""".stripMargin)
val pactConfiguration: Map[String, Value] = Map()
val avroRecord = AvroRecord(schema, pactConfiguration).value
val addressSchema = schema.getField("address").schema().getTypes.asScala.filterNot(_.getType == NULL).head
val addressRecord = new GenericData.Record(addressSchema)
addressRecord.put("street", "first street")

"a method," which {
"returns GenericRecord with field containing default value" in {
val genericRecord = avroRecord.toGenericRecord(schema)
genericRecord.get("address") shouldBe null
}
"returns empty matching rules using JsonPath" in {
avroRecord.matchingRules shouldBe empty
}
}
}

"value not provided" should provide {
val pactConfiguration: Map[String, Value] = Map()
val avroRecord = AvroRecord(schema, pactConfiguration).value

"a method," which {
"returns GenericRecord with field containing default value" in {
val genericRecord = avroRecord.toGenericRecord(schema)
genericRecord.get("address") shouldBe null
}
"returns empty matching rules using JsonPath" in {
avroRecord.matchingRules shouldBe empty
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import org.scalatest.wordspec.AnyWordSpecLike

class AvroRecordPrimitiveTypesTest extends AnyWordSpecLike with Matchers with EitherValues {
import com.github.austek.plugin.avro.utils.MatchingRuleCategoryImplicits.given

private val byteValue = "\\\u0000\\\u0001\\\u0002\\\u0003\\\u0004\\\u0005\\\u0006\\\u0007"
def provide: AfterWord = afterWord("provide")

"String field" when {
Expand Down Expand Up @@ -45,7 +47,7 @@ class AvroRecordPrimitiveTypesTest extends AnyWordSpecLike with Matchers with Ei
genericRecord.get("street") shouldBe "NONE"
}
"returns empty matching rules using JsonPath" in {
avroRecord.matchingRules.getRules("$.street") shouldBe empty
avroRecord.matchingRules shouldBe empty
}
}
}
Expand Down Expand Up @@ -81,7 +83,7 @@ class AvroRecordPrimitiveTypesTest extends AnyWordSpecLike with Matchers with Ei
genericRecord.get("no").toString.toInt shouldBe 5
}
"returns empty matching rules using JsonPath" in {
avroRecord.matchingRules.getRules("$.no") shouldBe empty
avroRecord.matchingRules shouldBe empty
}
}
}
Expand Down Expand Up @@ -117,7 +119,7 @@ class AvroRecordPrimitiveTypesTest extends AnyWordSpecLike with Matchers with Ei
genericRecord.get("id").toString.toInt shouldBe 100
}
"returns empty matching rules using JsonPath" in {
avroRecord.matchingRules.getRules("$.id") shouldBe empty
avroRecord.matchingRules shouldBe empty
}
}
}
Expand Down Expand Up @@ -153,7 +155,7 @@ class AvroRecordPrimitiveTypesTest extends AnyWordSpecLike with Matchers with Ei
genericRecord.get("width").toString.toDouble shouldBe 1.8d
}
"returns empty matching rules using JsonPath" in {
avroRecord.matchingRules.getRules("$.width") shouldBe empty
avroRecord.matchingRules shouldBe empty
}
}
}
Expand Down Expand Up @@ -189,7 +191,7 @@ class AvroRecordPrimitiveTypesTest extends AnyWordSpecLike with Matchers with Ei
genericRecord.get("height").toString.toFloat shouldBe 15.8f
}
"returns empty matching rules using JsonPath" in {
avroRecord.matchingRules.getRules("$.height") shouldBe empty
avroRecord.matchingRules shouldBe empty
}
}
}
Expand Down Expand Up @@ -225,7 +227,7 @@ class AvroRecordPrimitiveTypesTest extends AnyWordSpecLike with Matchers with Ei
genericRecord.get("enabled").toString.toBoolean shouldBe true
}
"returns empty matching rules using JsonPath" in {
avroRecord.matchingRules.getRules("$.enabled") shouldBe empty
avroRecord.matchingRules shouldBe empty
}
}
}
Expand All @@ -234,15 +236,13 @@ class AvroRecordPrimitiveTypesTest extends AnyWordSpecLike with Matchers with Ei
"Bytes field" when {
"value provided" should provide {
val schema = schemaWithField("""{"name": "MAC", "type": "bytes"}""")
val pactConfiguration: Map[String, Value] = Map(
"MAC" -> Value(StringValue("matching(equalTo, '\\\u0000\\\u0001\\\u0002\\\u0003\\\u0004\\\u0005\\\u0006\\\u0007')"))
)
val pactConfiguration: Map[String, Value] = Map("MAC" -> Value(StringValue(s"matching(equalTo, '$byteValue')")))
val avroRecord = AvroRecord(schema, pactConfiguration).value

"a method," which {
"returns GenericRecord with field" in {
val genericRecord = avroRecord.toGenericRecord(schema)
genericRecord.get("MAC") shouldBe "\\\u0000\\\u0001\\\u0002\\\u0003\\\u0004\\\u0005\\\u0006\\\u0007"
genericRecord.get("MAC") shouldBe byteValue
}
"returns matching rules using JsonPath" in {
avroRecord.matchingRules should have size 1
Expand All @@ -263,7 +263,58 @@ class AvroRecordPrimitiveTypesTest extends AnyWordSpecLike with Matchers with Ei
genericRecord.get("MAC") shouldBe "\\u0000"
}
"returns empty matching rules using JsonPath" in {
avroRecord.matchingRules.getRules("$.MAC") shouldBe empty
avroRecord.matchingRules shouldBe empty
}
}
}
}

"Optional Primitive field" when {
val schema = schemaWithField("""{"name": "MAC", "type": ["null", "bytes"]}""")
"value provided" should provide {
val pactConfiguration: Map[String, Value] = Map("MAC" -> Value(StringValue(s"matching(equalTo, '$byteValue')")))
val avroRecord = AvroRecord(schema, pactConfiguration).value

"a method," which {
"returns GenericRecord with field" in {
val genericRecord = avroRecord.toGenericRecord(schema)
genericRecord.get("MAC") shouldBe byteValue
}
"returns matching rules using JsonPath" in {
avroRecord.matchingRules should have size 1
val rules = avroRecord.matchingRules.getRules("$.MAC")
rules shouldBe Seq(EqualsMatcher.INSTANCE)
}
}
}

"value not provided but has default" should provide {
val schema = schemaWithField("""{"name": "MAC", "type": ["null", "bytes"], "default": null}""")
val pactConfiguration: Map[String, Value] = Map()
val avroRecord = AvroRecord(schema, pactConfiguration).value

"a method," which {
"returns GenericRecord with field containing default value" in {
val genericRecord = avroRecord.toGenericRecord(schema)
genericRecord.get("MAC") shouldBe null
}
"returns empty matching rules using JsonPath" in {
avroRecord.matchingRules shouldBe empty
}
}
}

"value not provided" should provide {
val pactConfiguration: Map[String, Value] = Map()
val avroRecord = AvroRecord(schema, pactConfiguration).value

"a method," which {
"returns GenericRecord with field containing default value" in {
val genericRecord = avroRecord.toGenericRecord(schema)
genericRecord.get("MAC") shouldBe null
}
"returns empty matching rules using JsonPath" in {
avroRecord.matchingRules shouldBe empty
}
}
}
Expand Down

0 comments on commit 37b4c1d

Please sign in to comment.