Skip to content

Commit

Permalink
Use State for handling json parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
Yannick Gladow committed Jul 29, 2019
1 parent d3f823f commit 913e4b9
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 66 deletions.
Expand Up @@ -38,7 +38,7 @@ object executeAccessPattern {
.map { case (name, cc) => cc + s"\nscala.reflect.classTag[$name].runtimeClass" }
.mkString("\n")
code = buildCode(access, caseClassesWithReflection)
tree <- Try(tb.parse(code)).toEither
tree <- Try(tb.parse(code)).toEither
result <- Try(tb.eval(tree).asInstanceOf[Json => String](parsedJson)).toEither
} yield result
}
Expand Down
124 changes: 72 additions & 52 deletions src/main/scala/io/yannick_cw/sjq/jsonToCaseClass.scala
@@ -1,7 +1,9 @@
package io.yannick_cw.sjq

import cats.data.{NonEmptyList, State}
import cats.data.State._
import cats.syntax.traverse._
import cats.instances.map.catsKernelStdMonoidForMap
import cats.instances.int.catsKernelStdGroupForInt
import cats.syntax.foldable._
import cats.instances.list._
import cats.kernel.Semigroup
Expand All @@ -10,7 +12,7 @@ import io.circe.Json
object jsonToCaseClass {
case class CC(name: String, content: Map[String, String])
def apply(j: Json): List[(String, String)] = {
val (_, allCCs) = buildCC(j, "CC", List.empty)
val allCCs = buildCC(j, "CC").runS(T(List.empty)).value.doneCCs
val ccs =
allCCs
.map(cc => cc.copy(content = cc.content.filter { case (key, value) => key.nonEmpty && value.nonEmpty }))
Expand All @@ -21,62 +23,80 @@ object jsonToCaseClass {
private def renderCC(cc: CC): String =
s"case class ${cc.name}(${cc.content.map { case (key, value) => s"$key: $value" }.mkString(", ")})"

private def buildCC(j: Json, nextLevelName: String, doneCCs: List[CC]): (Option[String], List[CC]) = {
j.fold(
Some("Null") -> doneCCs,
_ => Some("Boolean") -> doneCCs,
_ => Some("Double") -> doneCCs,
_ => Some("String") -> doneCCs,
array =>
array.toList match {
case all @ ele :: rest if all.forall(_.isObject) =>
val innerType =
rest.foldLeft(buildCC(ele, nextLevelName, doneCCs)) {
case ((newValueName, aggCCs), nextJson) =>
newValueName -> buildCC(nextJson, nextLevelName, aggCCs)._2
}
val (valueName, ccs) = innerType match {
case (Some(valueName), Nil) => valueName -> doneCCs
case (Some(valueName), ele :: Nil) => valueName -> (ele :: Nil)
case (Some(valueName), ele :: more) =>
val (nextLevelCCs, otherCCs) = (ele :: more)
.partition(_.name.startsWith(valueName))
private def addKV(value: String, nextLevelName: String): S =
pure((cc: CC) => cc.copy(content = cc.content.+((nextLevelName, value))))

implicit val sSemi: Semigroup[String] =
(x: String, y: String) => if (x == "Null") s"Option[$y]" else if (y == "Null") s"Option[$x]" else x
val allContent = nextLevelCCs.map(_.content).combineAll
val keyCountPerContent =
nextLevelCCs.map(cc => cc.content.map[String, Int] { case (key, _) => (key, 1) }).combineAll
case class T(doneCCs: List[CC])
type S = State[T, CC => CC]

val keyCount = nextLevelCCs.size
private def mergeCCs(nextLevelContent: List[Map[String, String]]) = {
implicit val sSemi: Semigroup[String] =
(x: String, y: String) =>
if (x == "Null" && y != "Null") s"Option[$y]" else if (y == "Null" && x != "Null") s"Option[$x]" else x
val commonKeys = NonEmptyList
.fromList(nextLevelContent)
.map(list => list.tail.foldLeft(list.head.keySet)((commonKeys, next) => commonKeys.intersect(next.keySet)))
.getOrElse(Set.empty)
nextLevelContent.combineAll.toList.map {
case (key, value) if commonKeys.contains(key) => (key, value)
case (key, value) => (key, s"Option[$value]")
}
}

val newCC = ele.copy(content = keyCountPerContent.map({
case (key, count) if count == keyCount => key -> allContent(key)
case (key, _) => key -> s"Option[${allContent(key)}]"
}))
valueName -> (newCC :: otherCCs)
case _ => "" -> doneCCs
}
private def mergeNextLevelsCCs(nextLevelName: String, t: T) = {
val nextLevelsCcs = t.doneCCs
.filter(_.name == nextLevelName)
.map(_.content)
val mergedCCs = mergeCCs(nextLevelsCcs)
t.copy(
doneCCs =
(if (mergedCCs.nonEmpty) List(CC(nextLevelName, mergedCCs.toMap))
else List.empty) ::: t.doneCCs.filterNot(cc => cc.name == nextLevelName))
}

Some(s"List[$valueName]") -> ccs
case ele :: _ =>
val (newValue, allCCs) = buildCC(ele, nextLevelName, doneCCs)
newValue.map(v => s"List[$v]") -> allCCs
// todo fix if things have different types
case _ => Some("List[String]") -> doneCCs
},
jObj => {
val (allNewCCs, newCC) = jObj.toMap.foldLeft(doneCCs -> CC(nextLevelName, Map.empty)) {
case ((allCCs, currCC), (key, value)) =>
val safeNextLevelName = findFreeName(currCC :: allCCs, key)
val (newValue, allNewCCs) = buildCC(value, safeNextLevelName, allCCs)
allNewCCs -> newValue.map(v => currCC.copy(content = currCC.content.updated(key, v))).getOrElse(currCC)
}
(Some(nextLevelName), newCC :: allNewCCs)
}
)
private def buildNextLevelType(allNextLevelTypes: Option[NonEmptyList[String]]) = {
allNextLevelTypes
.map(
nel =>
if (nel.forall(_ == nel.head)) nel.head
else "Json")
.getOrElse("String")
}

private def buildCC(j: Json, nextLevelName: String): S = j.fold(
addKV("Null", nextLevelName),
_ => addKV("Boolean", nextLevelName),
_ => addKV("Double", nextLevelName),
_ => addKV("String", nextLevelName),
array =>
for {
ccModifications <- array.toList.traverse(buildCC(_, nextLevelName))
_ <- modify[T](mergeNextLevelsCCs(nextLevelName, _))
value = ccModifications.map(f => f(CC("", Map.empty)))
allNextLevelTypes = NonEmptyList.fromList(
value.flatMap(_.content.filter(tuple => nextLevelName.startsWith(tuple._1)).values))
nextLevelTypeName = buildNextLevelType(allNextLevelTypes)
} yield
(cc: CC) => cc.copy(content = cc.content.+((cleanAddedField(nextLevelName), s"List[${nextLevelTypeName}]"))),
jObj => {
for {
currentState <- get[T]
ccModifications <- jObj.toMap.toList.traverse {
case (key, value) =>
val safeNextLevelName = findFreeName(currentState.doneCCs, key)
buildCC(value, safeNextLevelName)
}
updatedCC = ccModifications.foldLeft(CC(nextLevelName, Map.empty))((cc, ccOp) => ccOp(cc))
_ <- modify[T](t => t.copy(doneCCs = updatedCC :: t.doneCCs))
} yield
(cc: CC) =>
cc.copy(content = cc.content.+((cleanAddedField(nextLevelName), nextLevelName))) // ignored in first run
}
)

@scala.annotation.tailrec
private def findFreeName(ccs: List[CC], name: String): String =
if (ccs.exists(_.name == name)) findFreeName(ccs, name + "1") else name

private def cleanAddedField(nextLevelName: String): String = nextLevelName.reverse.dropWhile(_ == '1').reverse
}
30 changes: 17 additions & 13 deletions src/test/scala/io/yannick_cw/sjq/jsonToCaseClassTest.scala
Expand Up @@ -74,13 +74,16 @@ class jsonToCaseClassTest extends FlatSpec with Matchers {
| {
| "things": [
| { "id": null },
| { "id": 22 }
| { "id": 22, "more": { "name": "Xx" } }
| ]
| }
""".stripMargin).fold(throw _, x => x)

jsonToCaseClass(json) shouldBe List(("CC", "case class CC(things: List[things])"),
("things", "case class things(id: Option[Double])"))
jsonToCaseClass(json) shouldBe List(
("CC", "case class CC(things: List[things])"),
("things", "case class things(more: Option[more], id: Option[Double])"),
("more", "case class more(name: String)")
)
}

it should "parse json empty lists" in {
Expand All @@ -94,32 +97,34 @@ class jsonToCaseClassTest extends FlatSpec with Matchers {
}

it should "parse json with nested objects in arrays" in {
val json = parse("""
val json =
parse("""
|{
| "hotels": {
| "entities": [
| { "id": "1aa4c4ad-f9ea-3367-a163-8a3a6884d450", "name": "Dana Beach Resort" }
| { "id": "1aa4c4ad-f9ea-3367-a163-8a3a6884d450", "name": "Dana Beach Resort", "ids": [1,2,3] }
| ]
| }
|}""".stripMargin)
.fold(throw _, x => x)
.fold(throw _, x => x)

jsonToCaseClass(json) shouldBe List(
("CC", "case class CC(hotels: hotels)"),
("hotels", "case class hotels(entities: List[entities])"),
("entities", "case class entities(id: String, name: String)")
("entities", "case class entities(name: String, ids: List[Double], id: String)")
)
}

it should "parse json with different types in objects in arrays making them optional" in {
val json = parse("""
| {
| "list": [ { "id": 22 }, { "name": "Toben" } ]
| "list": [ { "id": 22, "other": 12 }, { "name": "Toben", "other": 12} ]
| }
""".stripMargin).fold(throw _, x => x)

jsonToCaseClass(json) shouldBe List(("CC", "case class CC(list: List[list])"),
("list", "case class list(name: Option[String], id: Option[Double])"))
jsonToCaseClass(json) shouldBe List(
("CC", "case class CC(list: List[list])"),
("list", "case class list(other: Double, name: Option[String], id: Option[Double])"))
}

it should "parse json with different types in objects in arrays making only the always optional ones optional" in {
Expand Down Expand Up @@ -176,16 +181,15 @@ class jsonToCaseClassTest extends FlatSpec with Matchers {
)
}

//todo
ignore should "work for different types in an array" in {
it should "work for different types in an array" in {
val json = parse("""
| {
| "list": [ 22, "Hi", false ]
| }
""".stripMargin).fold(throw _, x => x)

jsonToCaseClass(json) shouldBe List(
("CC", "case class CC(list: List[Any])")
("CC", "case class CC(list: List[Json])")
)
}
}

0 comments on commit 913e4b9

Please sign in to comment.