Skip to content

Commit

Permalink
xml fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
agolovenko committed Dec 7, 2021
1 parent 16a2f6a commit b751b27
Show file tree
Hide file tree
Showing 22 changed files with 143 additions and 156 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ ThisBuild / licenses += ("Apache-2.0", url("http://www.apache.org/licenses/LICEN
ThisBuild / publishMavenStyle := true
ThisBuild / publishConfiguration := publishConfiguration.value.withOverwrite(true)
ThisBuild / publishTo := Some(if (isSnapshot.value) sonatypeSnapshots else sonatypeStaging)
ThisBuild / scalaVersion := scala212
ThisBuild / scalaVersion := scala213
ThisBuild / crossScalaVersions := supportedScalaVersions
ThisBuild / versionScheme := Some("early-semver")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.github.agolovenko.avro

import org.apache.avro.Schema

import scala.util.control.NoStackTrace

class ParserException(message: String)(implicit path: Path) extends RuntimeException(s"$message @ $path") with NoStackTrace

class WrongTypeException(schema: Schema, value: String, explanations: Seq[String] = Seq.empty)(implicit path: Path)
extends ParserException(
s"Failed to extract ${typeName(schema)} from $value${if (explanations.isEmpty) "" else explanations.mkString("\nCaused by:\n", "\nand\n", "")}"
)

class MissingValueException(schema: Schema)(implicit path: Path) extends ParserException(s"Missing ${typeName(schema)} node")
41 changes: 38 additions & 3 deletions core/src/main/scala/io/github/agolovenko/avro/package.scala
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package io.github.agolovenko

import org.apache.avro.Schema
import org.apache.avro.Schema.Type.{ENUM, UNION}
import org.apache.avro.Schema.Type._
import org.apache.avro.generic.{GenericData, GenericDatumReader, GenericDatumWriter}
import org.apache.avro.io.{DecoderFactory, EncoderFactory}
import org.apache.avro.{JsonProperties, Schema}

import java.io.ByteArrayOutputStream
import java.lang.{Boolean => JBool, Double => JDouble, Float => JFloat, Integer => JInt, Long => JLong}
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import java.util.Base64
import java.util.{Base64, List => JList, Map => JMap}
import scala.collection.compat._
import scala.jdk.CollectionConverters._

package object avro {
Expand Down Expand Up @@ -40,4 +43,36 @@ package object avro {
}

def toBase64(bytes: Array[Byte]): String = new String(Base64.getEncoder.encode(bytes), StandardCharsets.UTF_8)

private[avro] def extractDefaultValue(defaultValue: Any, schema: Schema)(implicit path: Path): Any = (schema.getType, defaultValue) match {
case (NULL, JsonProperties.NULL_VALUE) => null
case (STRING, value: String) => value
case (ENUM, value: String) => value
case (INT, value: JInt) => value.intValue()
case (LONG, value: JLong) => value.longValue()
case (FLOAT, value: JFloat) => value.floatValue()
case (DOUBLE, value: JDouble) => value.doubleValue()
case (BOOLEAN, value: JBool) => value.booleanValue()
case (BYTES, value: Array[Byte]) => ByteBuffer.wrap(value)
case (FIXED, value: Array[Byte]) => value

case (ARRAY, list: JList[_]) =>
val extracted = list.asScala.map { extractDefaultValue(_, schema.getElementType) }
new GenericData.Array(schema, extracted.asJava)

case (MAP, map: JMap[_, _]) =>
map.asScala.view.mapValues { extractDefaultValue(_, schema.getValueType) }.toMap.asJava

case (RECORD, map: JMap[_, _]) =>
val result = new GenericData.Record(schema)
map.asScala.foreach {
case (k, value) =>
val key = k.asInstanceOf[String]
val extracted = extractDefaultValue(value, schema.getField(key).schema())
result.put(key, extracted)
}
result

case _ => throw new ParserException(s"Unsupported default value $defaultValue for type ${schema.getType}")
}
}
95 changes: 36 additions & 59 deletions json/src/main/scala/io/github/agolovenko/avro/json/JsonParser.scala
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
package io.github.agolovenko.avro.json

import io.github.agolovenko.avro.{FieldRenamings, Path, typeName}
import io.github.agolovenko.avro._
import org.apache.avro.Schema
import org.apache.avro.Schema.Type._
import org.apache.avro.generic.GenericData
import org.apache.avro.{JsonProperties, Schema}
import play.api.libs.json._

import java.lang.{Boolean => JBool, Double => JDouble, Float => JFloat, Integer => JInt, Long => JLong}
import java.nio.ByteBuffer
import java.util.{HashMap => JHashMap, List => JList, Map => JMap}
import scala.collection.compat._
import java.util.{HashMap => JHashMap, Map => JMap}
import scala.jdk.CollectionConverters._
import scala.util.Try
import scala.util.control.NonFatal
Expand All @@ -20,7 +17,7 @@ class JsonParser(schema: Schema, stringParsers: Map[String, String => Any] = Map
if (schema.getType == RECORD)
readRecord(JsDefined(data), schema, defaultValue = None)
else
throw new JsonParserException(s"Unsupported root schema of type ${schema.getType}")
throw new ParserException(s"Unsupported root schema of type ${schema.getType}")
}

private def readAny(data: JsLookupResult, schema: Schema, defaultValue: Option[Any])(implicit path: Path): Any = schema.getType match {
Expand Down Expand Up @@ -49,20 +46,24 @@ class JsonParser(schema: Schema, stringParsers: Map[String, String => Any] = Map
schema.getFields.asScala.foreach { field =>
val fieldName = fieldRenamings(field.name())
path.push(fieldName)
val value = readAny(obj \ fieldName, field.schema(), Option(field.defaultVal()))
result.put(field.name(), value)
path.pop()
try {
val value = readAny(obj \ fieldName, field.schema(), Option(field.defaultVal()))
result.put(field.name(), value)
} finally {
path.pop()
()
}
}
result
case JsDefined(otherNode) => throw new WrongTypeException(schema, otherNode)
case JsDefined(otherNode) => throw new WrongTypeException(schema, otherNode.toString())
case _ => fallbackToDefault(defaultValue, schema).asInstanceOf[GenericData.Record]
}

private def readEnum(data: JsLookupResult, schema: Schema, defaultValue: Option[Any])(implicit path: Path): GenericData.EnumSymbol = {
val symbol = read[String](data, schema, defaultValue)

if (schema.getEnumSymbols.contains(symbol)) new GenericData.EnumSymbol(schema, symbol)
else throw new WrongTypeException(schema, data.get)
else throw new WrongTypeException(schema, data.get.toString())
}

private def readArray(data: JsLookupResult, schema: Schema, defaultValue: Option[Any])(implicit path: Path): GenericData.Array[Any] =
Expand All @@ -72,12 +73,16 @@ class JsonParser(schema: Schema, stringParsers: Map[String, String => Any] = Map
arr.value.zipWithIndex.foreach {
case (jsValue, idx) =>
path.push(s"[$idx]")
val value = readAny(JsDefined(jsValue), schema.getElementType, None)
result.add(idx, value)
path.pop()
try {
val value = readAny(JsDefined(jsValue), schema.getElementType, None)
result.add(idx, value)
} finally {
path.pop()
()
}
}
result
case JsDefined(otherNode) => throw new WrongTypeException(schema, otherNode)
case JsDefined(otherNode) => throw new WrongTypeException(schema, otherNode.toString())
case _ => fallbackToDefault(defaultValue, schema).asInstanceOf[GenericData.Array[Any]]
}

Expand All @@ -88,12 +93,16 @@ class JsonParser(schema: Schema, stringParsers: Map[String, String => Any] = Map
obj.value.foreach {
case (key, jsValue) =>
path.push(key)
val value = readAny(JsDefined(jsValue), schema.getValueType, None)
result.put(key, value)
path.pop()
try {
val value = readAny(JsDefined(jsValue), schema.getValueType, None)
result.put(key, value)
} finally {
path.pop()
()
}
}
result
case JsDefined(otherNode) => throw new WrongTypeException(schema, otherNode)
case JsDefined(otherNode) => throw new WrongTypeException(schema, otherNode.toString())
case _ => fallbackToDefault(defaultValue, schema).asInstanceOf[JMap[String, Any]]
}

Expand All @@ -108,8 +117,8 @@ class JsonParser(schema: Schema, stringParsers: Map[String, String => Any] = Map
else
data match {
case JsDefined(otherNode) =>
val explanation = unionIt.flatMap(_.failed.map(_.getMessage).toOption).mkString("; ")
throw new WrongTypeException(schema, otherNode, Some(explanation))
val explanations = unionIt.flatMap(_.failed.map(_.getMessage).toOption).toSeq
throw new WrongTypeException(schema, otherNode.toString(), explanations)
case _ => throw new MissingValueException(schema)
}
}
Expand All @@ -121,7 +130,7 @@ class JsonParser(schema: Schema, stringParsers: Map[String, String => Any] = Map
val bytes = readBytes(data, schema, defaultValue).asInstanceOf[Array[Byte]]

if (bytes.length == schema.getFixedSize) new GenericData.Fixed(schema, bytes)
else throw new WrongTypeException(schema, data.get, Some(s"incorrect size: ${bytes.length} instead of ${schema.getFixedSize}"))
else throw new WrongTypeException(schema, data.get.toString(), Seq(s"incorrect size: ${bytes.length} instead of ${schema.getFixedSize}"))
}

private def read[T: Reads](data: JsLookupResult, schema: Schema, defaultValue: Option[Any])(implicit path: Path): Any = data match {
Expand All @@ -130,7 +139,7 @@ class JsonParser(schema: Schema, stringParsers: Map[String, String => Any] = Map
value
.validate[T]
.fold(
invalid = _ => throw new WrongTypeException(schema, value),
invalid = _ => throw new WrongTypeException(schema, value.toString()),
valid = identity
)
case _ => fallbackToDefault(defaultValue, schema)
Expand All @@ -139,53 +148,21 @@ class JsonParser(schema: Schema, stringParsers: Map[String, String => Any] = Map
private def parseString(str: String, schema: Schema)(implicit path: Path): Any = {
if (schema.getType == STRING || schema.getType == ENUM) str
else
stringParsers.get(typeName(schema)).fold(throw new WrongTypeException(schema, JsString(str), Some("no string parser supplied"))) { parser =>
stringParsers.get(typeName(schema)).fold(throw new WrongTypeException(schema, str, Seq("no string parser supplied"))) { parser =>
try {
parser(str)
} catch {
case NonFatal(e) => throw new WrongTypeException(schema, JsString(str), Option(e.getMessage))
case NonFatal(e) => throw new WrongTypeException(schema, str, Seq(e.getMessage))
}
}
}

private def readNull(data: JsLookupResult, schema: Schema, defaultValue: Option[Any])(implicit path: Path): Null = data match {
case JsDefined(JsNull) => null
case JsDefined(otherNode) => throw new WrongTypeException(schema, otherNode)
case JsDefined(otherNode) => throw new WrongTypeException(schema, otherNode.toString())
case _ => fallbackToDefault(defaultValue, schema).asInstanceOf[Null]
}

private def fallbackToDefault(defaultValue: Option[Any], schema: Schema)(implicit path: Path): Any =
defaultValue.fold(throw new MissingValueException(schema)) { extractDefaultValue(_, schema) }

private def extractDefaultValue(defaultValue: Any, schema: Schema)(implicit path: Path): Any = (schema.getType, defaultValue) match {
case (NULL, JsonProperties.NULL_VALUE) => null
case (STRING, value: String) => value
case (ENUM, value: String) => value
case (INT, value: JInt) => value.intValue()
case (LONG, value: JLong) => value.longValue()
case (FLOAT, value: JFloat) => value.floatValue()
case (DOUBLE, value: JDouble) => value.doubleValue()
case (BOOLEAN, value: JBool) => value.booleanValue()
case (BYTES, value: Array[Byte]) => ByteBuffer.wrap(value)
case (FIXED, value: Array[Byte]) => value

case (ARRAY, list: JList[_]) =>
val extracted = list.asScala.map { extractDefaultValue(_, schema.getElementType) }
new GenericData.Array(schema, extracted.asJava)

case (MAP, map: JMap[_, _]) =>
map.asScala.view.mapValues { extractDefaultValue(_, schema.getValueType) }.toMap.asJava

case (RECORD, map: JMap[_, _]) =>
val result = new GenericData.Record(schema)
map.asScala.foreach {
case (k, value) =>
val key = k.asInstanceOf[String]
val extracted = extractDefaultValue(value, schema.getField(key).schema())
result.put(key, extracted)
}
result

case _ => throw new JsonParserException(s"Unsupported default value $defaultValue for type ${schema.getType}")
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.agolovenko.avro.json

import io.github.agolovenko.avro.{MissingValueException, WrongTypeException}
import org.apache.avro.Schema
import org.apache.avro.generic.GenericData
import org.scalatest.matchers.should.Matchers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.github.agolovenko.avro.json

import io.github.agolovenko.avro.StringParsers
import io.github.agolovenko.avro.{MissingValueException, StringParsers, WrongTypeException}
import org.apache.avro.Schema
import org.apache.avro.generic.GenericData
import org.scalatest.matchers.should.Matchers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.github.agolovenko.avro.json

import io.github.agolovenko.avro.{StringParsers, toBase64}
import io.github.agolovenko.avro.{MissingValueException, StringParsers, WrongTypeException, toBase64}
import org.apache.avro.Schema
import org.apache.avro.generic.GenericData
import org.scalatest.matchers.should.Matchers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.github.agolovenko.avro.json

import io.github.agolovenko.avro.StringParsers
import io.github.agolovenko.avro.{MissingValueException, StringParsers, WrongTypeException}
import org.apache.avro.Schema
import org.apache.avro.generic.GenericData
import org.scalatest.matchers.should.Matchers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.agolovenko.avro.json

import io.github.agolovenko.avro.{MissingValueException, ParserException}
import org.apache.avro.Schema
import org.apache.avro.generic.GenericData
import org.scalatest.matchers.should.Matchers
Expand Down Expand Up @@ -33,7 +34,7 @@ class EnumSpec extends AnyWordSpec with Matchers {

"fails on invalid value" in {
val data = Json.parse("""{"field1": "ev33"}""")
a[JsonParserException] should be thrownBy new JsonParser(schema)(data)
a[ParserException] should be thrownBy new JsonParser(schema)(data)
}

"applies default value" in {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.github.agolovenko.avro.json

import io.github.agolovenko.avro.{StringParsers, toBase64}
import io.github.agolovenko.avro.{MissingValueException, StringParsers, WrongTypeException, toBase64}
import org.apache.avro.Schema
import org.apache.avro.generic.GenericData
import org.scalatest.matchers.should.Matchers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.github.agolovenko.avro.json

import io.github.agolovenko.avro.StringParsers
import io.github.agolovenko.avro.{MissingValueException, StringParsers, WrongTypeException}
import org.apache.avro.Schema
import org.apache.avro.generic.GenericData
import org.scalatest.matchers.should.Matchers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.github.agolovenko.avro.json

import io.github.agolovenko.avro.StringParsers
import io.github.agolovenko.avro.{MissingValueException, StringParsers, WrongTypeException}
import org.apache.avro.Schema
import org.apache.avro.generic.GenericData
import org.scalatest.matchers.should.Matchers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.github.agolovenko.avro.json

import io.github.agolovenko.avro.StringParsers
import io.github.agolovenko.avro.{MissingValueException, StringParsers, WrongTypeException}
import org.apache.avro.Schema
import org.apache.avro.generic.GenericData
import org.scalatest.matchers.should.Matchers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.agolovenko.avro.json

import io.github.agolovenko.avro.{MissingValueException, WrongTypeException}
import org.apache.avro.Schema
import org.apache.avro.generic.GenericData
import org.scalatest.matchers.should.Matchers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.agolovenko.avro.json

import io.github.agolovenko.avro.{MissingValueException, WrongTypeException}
import org.apache.avro.generic.GenericData
import org.apache.avro.{JsonProperties, Schema}
import org.scalatest.matchers.should.Matchers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.agolovenko.avro.json

import io.github.agolovenko.avro.MissingValueException
import org.apache.avro.Schema
import org.apache.avro.generic.GenericData
import org.scalatest.matchers.should.Matchers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.github.agolovenko.avro.json

import io.github.agolovenko.avro.StringParsers
import io.github.agolovenko.avro.{StringParsers, WrongTypeException}
import org.apache.avro.generic.GenericData
import org.apache.avro.{LogicalTypes, Schema}
import org.scalatest.matchers.should.Matchers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.agolovenko.avro.json

import io.github.agolovenko.avro.{MissingValueException, WrongTypeException}
import org.apache.avro.generic.GenericData
import org.apache.avro.{JsonProperties, Schema}
import org.scalatest.matchers.should.Matchers
Expand Down

0 comments on commit b751b27

Please sign in to comment.