├── project ├── build.properties ├── plugins.sbt └── Dependencies.scala ├── .scalafmt.conf ├── version.sbt ├── .gitignore ├── .travis.yml ├── src ├── main │ └── scala │ │ └── io │ │ └── yannick_cw │ │ └── sjq │ │ ├── Cli.scala │ │ ├── executeAccessPattern.scala │ │ └── jsonToCaseClass.scala ├── it │ └── scala │ │ └── io │ │ └── yannick_cw │ │ └── sjq │ │ └── ExecuteAccessPatternTest.scala └── test │ └── scala │ └── io │ └── yannick_cw │ └── sjq │ └── jsonToCaseClassTest.scala └── README.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.8 2 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | maxColumn = 120 2 | align = more 3 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "0.0.4-SNAPSHOT" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | **/target 3 | .DS_STORE 4 | .dev 5 | *.iml 6 | /pipeline/acceptance_test/docker-compose.yml 7 | **/report 8 | doc/result 9 | result 10 | .bloop 11 | **/.bloop 12 | .metals -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.lucidchart" % "sbt-scalafmt" % "1.16") 2 | addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.1.7") 3 | addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.25") 4 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.11") 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | 3 | scala: 4 | - 2.13.0 5 | 6 | script: "sbt test it:test" 7 | 8 | cache: 9 | directories: 10 | - $HOME/.ivy2/cache 11 | - $HOME/.sbt/boot/ 12 | 13 | before_cache: 14 | # Tricks to avoid unnecessary cache updates 15 | - find $HOME/.ivy2 -name "ivydata-*.properties" -delete 16 | - find $HOME/.sbt -name "*.lock" -delete 17 | 18 | jdk: 19 | - oraclejdk11 20 | 21 | -------------------------------------------------------------------------------- /src/main/scala/io/yannick_cw/sjq/Cli.scala: -------------------------------------------------------------------------------- 1 | package io.yannick_cw.sjq 2 | import caseapp._ 3 | 4 | import scala.concurrent.duration._ 5 | import scala.concurrent.{Await, Future} 6 | import scala.concurrent.ExecutionContext.Implicits.global 7 | import scala.util.Try 8 | 9 | case class CliArgs(@ExtraName("a") 10 | access: String, 11 | @ExtraName("j") 12 | json: Option[String]) 13 | 14 | object Cli extends CaseApp[CliArgs] { 15 | 16 | private def readIn = 17 | Try(Await.result(Future(scala.io.StdIn.readLine()), 10.millis)).toEither 18 | 19 | override def run(options: CliArgs, remainingArgs: RemainingArgs): Unit = { 20 | val res = for { 21 | json <- options.json 22 | .fold(readIn)(Right(_)) 23 | .left 24 | .map(_ => new Exception("Neither --json argument or json piped | was given")) 25 | result <- executeAccessPattern(options.access, json) 26 | } yield result 27 | 28 | println(res.fold(_.getMessage, x => x)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Versions { 4 | val circe = "0.12.0-RC2" 5 | val caseApp = "2.0.0-M9" 6 | val scalaTest = "3.0.8" 7 | val scalaCompiler = "2.13.0" 8 | val scalaCheck = "1.14.0" 9 | } 10 | 11 | object Dependencies { 12 | val caseApp = "com.github.alexarchambault" %% "case-app" % Versions.caseApp 13 | val scalaTest = "org.scalatest" %% "scalatest" % Versions.scalaTest 14 | val scalaCheck = "org.scalacheck" %% "scalacheck" % Versions.scalaCheck % "it" 15 | 16 | val circe = Seq( 17 | "io.circe" %% "circe-core", 18 | "io.circe" %% "circe-generic", 19 | "io.circe" %% "circe-parser", 20 | ).map(_ % Versions.circe) :+ "io.circe" %% "circe-testing" % Versions.circe % "it" 21 | 22 | val scalaCompiler = "org.scala-lang" % "scala-compiler" % Versions.scalaCompiler 23 | 24 | val dependencies = Seq( 25 | caseApp, 26 | scalaTest % "test,it", 27 | scalaCompiler, 28 | scalaCheck 29 | ) ++ circe 30 | } 31 | -------------------------------------------------------------------------------- /src/main/scala/io/yannick_cw/sjq/executeAccessPattern.scala: -------------------------------------------------------------------------------- 1 | package io.yannick_cw.sjq 2 | 3 | import io.circe.Json 4 | import io.circe.parser.parse 5 | 6 | import scala.reflect.runtime._ 7 | import scala.tools.reflect.ToolBox 8 | import scala.util.Try 9 | 10 | object executeAccessPattern { 11 | private def buildCode(access: String, ccs: String) = 12 | s""" 13 | | import io.circe.{Encoder, Decoder, HCursor, Json} 14 | | implicit val dec: Decoder[Null] = (c: HCursor) => Right(null) 15 | | implicit val enc: Encoder[Null] = (_: Null) => Json.Null 16 | | 17 | | (json: Json) => { 18 | | import io.circe.generic.auto._ 19 | | import io.circe._ 20 | | import io.circe.syntax._ 21 | | 22 | | $ccs 23 | | 24 | | val root = json.as[CC].getOrElse(null) 25 | | val result = $access 26 | | result.asJson.spaces2 27 | |} 28 | """.stripMargin 29 | 30 | private val cm = universe.runtimeMirror(getClass.getClassLoader) 31 | private val tb = cm.mkToolBox() 32 | 33 | def apply(access: String, json: String): Either[Throwable, String] = { 34 | for { 35 | parsedJson <- parse(json) 36 | caseClasses = jsonToCaseClass(parsedJson) 37 | caseClassesWithReflection = caseClasses 38 | .map { case (name, cc) => cc + s"\nscala.reflect.classTag[$name].runtimeClass" } 39 | .mkString("\n") 40 | code = buildCode(access, caseClassesWithReflection) 41 | tree <- Try(tb.parse(code)).toEither 42 | result <- Try(tb.eval(tree).asInstanceOf[Json => String](parsedJson)).toEither 43 | } yield result 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sjq 2 | 3 | ## About 4 | 5 | Use Scala syntax to modify json fast and however you want from the Commandline. 6 | 7 | Just pass `code` to modify the `root` case class. sjq parses the json to a case class and allows editing and modifying it in any way. 8 | 9 | e.g. 10 | 11 | ```bash 12 | sjq -a 'root.subclass.copy(name = root.subclass.name + "Jo")' -j '{ "subclass": { "name": "Ho", "ids": [22, 23, 24] }}' 13 | { 14 | "name" : "HoJo", 15 | "ids" : [ 16 | 22.0, 17 | 23.0, 18 | 24.0 19 | ] 20 | } 21 | ``` 22 | 23 | One example with a more complex json and a remote api: 24 | 25 | ##### Get all hotel names with a score over 300 26 | 27 | ``` 28 | curl https://www.holidaycheck.de/svc/search-api/search/mall\?tenant\=test \ 29 | | sjq -a 'root.destinations.entities.filter(_.rankingScore > 300).map(_.name)' 30 | ``` 31 | 32 | returns 33 | 34 | ``` 35 | [ 36 | "Mallorca", 37 | "Malles Venosta / Mals", 38 | "Palma de Mallorca" 39 | ] 40 | ``` 41 | 42 | ## Install 43 | 44 | `brew install yannick-cw/homebrew-tap/sjq` 45 | 46 | Or download the executable from the releases. 47 | 48 | ## Usage 49 | 50 | You can pass any valid scala code to access the json, the json is internally represented as a case class. 51 | 52 | The input to use is the case class `root` 53 | 54 | ```bash 55 | sjq -a 'root.subclass.ids.filter(id => id > 22)' -j '{ "subclass": { "name": "Ho", "ids": [22, 23, 24] }}' 56 | ## Results in 57 | [ 58 | 23.0, 59 | 24.0 60 | ] 61 | ``` 62 | 63 | ### Alternatively pipe input 64 | 65 | ```bash 66 | echo '{ "subclass": { "name": "Ho", "ids": [22, 23, 24] }}' | sjq -a 'root.subclass.ids.filter(id => id > 22)' 67 | ``` 68 | 69 | ## Planned features 70 | 71 | * support subclasses with the same name with different value types 72 | * interactive mode with auto complete on json 73 | -------------------------------------------------------------------------------- /src/main/scala/io/yannick_cw/sjq/jsonToCaseClass.scala: -------------------------------------------------------------------------------- 1 | package io.yannick_cw.sjq 2 | 3 | import cats.data.{NonEmptyList, State} 4 | import cats.data.State._ 5 | import cats.syntax.traverse._ 6 | import cats.instances.map.catsKernelStdMonoidForMap 7 | import cats.syntax.foldable._ 8 | import cats.instances.list._ 9 | import cats.kernel.Semigroup 10 | import io.circe.Json 11 | 12 | object jsonToCaseClass { 13 | case class CC(name: String, content: Map[String, String]) 14 | def apply(j: Json): List[(String, String)] = { 15 | val allCCs = buildCC(j, "CC").runS(T(List.empty)).value.doneCCs 16 | val ccs = 17 | allCCs 18 | .map(cc => cc.copy(content = cc.content.filter { case (key, value) => key.nonEmpty && value.nonEmpty })) 19 | 20 | ccs.map(cc => (cc.name, renderCC(cc))) 21 | } 22 | 23 | private def renderCC(cc: CC): String = 24 | s"case class ${cc.name}(${cc.content.map { case (key, value) => s"$key: $value" }.mkString(", ")})" 25 | 26 | private def addKV(value: String, nextLevelName: String): S = 27 | pure((cc: CC) => cc.copy(content = cc.content.+((nextLevelName, value)))) 28 | 29 | case class T(doneCCs: List[CC]) 30 | type S = State[T, CC => CC] 31 | 32 | private def mergeCCs(nextLevelContent: List[Map[String, String]]) = { 33 | implicit val sSemi: Semigroup[String] = 34 | (x: String, y: String) => 35 | if (x == "Null" && y != "Null") s"Option[$y]" else if (y == "Null" && x != "Null") s"Option[$x]" else x 36 | val commonKeys = NonEmptyList 37 | .fromList(nextLevelContent) 38 | .map(list => list.tail.foldLeft(list.head.keySet)((commonKeys, next) => commonKeys.intersect(next.keySet))) 39 | .getOrElse(Set.empty) 40 | nextLevelContent.combineAll.toList.map { 41 | case (key, value) if commonKeys.contains(key) => (key, value) 42 | case (key, value) => (key, s"Option[$value]") 43 | } 44 | } 45 | 46 | private def mergeNextLevelsCCs(nextLevelName: String, t: T) = { 47 | val nextLevelsCcs = t.doneCCs 48 | .filter(_.name == nextLevelName) 49 | .map(_.content) 50 | val mergedCCs = mergeCCs(nextLevelsCcs) 51 | t.copy( 52 | doneCCs = 53 | (if (mergedCCs.nonEmpty) List(CC(nextLevelName, mergedCCs.toMap)) 54 | else List.empty) ::: t.doneCCs.filterNot(cc => cc.name == nextLevelName)) 55 | } 56 | 57 | private def buildNextLevelType(allNextLevelTypes: Option[NonEmptyList[String]]) = { 58 | allNextLevelTypes 59 | .map( 60 | nel => 61 | if (nel.forall(_ == nel.head)) nel.head 62 | else "Json") 63 | .getOrElse("String") 64 | } 65 | 66 | private def buildCC(j: Json, nextLevelName: String): S = j.fold( 67 | addKV("Null", nextLevelName), 68 | _ => addKV("Boolean", nextLevelName), 69 | _ => addKV("Double", nextLevelName), 70 | _ => addKV("String", nextLevelName), 71 | array => 72 | for { 73 | ccModifications <- array.toList.traverse(buildCC(_, nextLevelName)) 74 | _ <- modify[T](mergeNextLevelsCCs(nextLevelName, _)) 75 | value = ccModifications.map(f => f(CC("", Map.empty))) 76 | allNextLevelTypes = NonEmptyList.fromList( 77 | value.flatMap(_.content.filter(tuple => nextLevelName.startsWith(tuple._1)).values)) 78 | nextLevelTypeName = buildNextLevelType(allNextLevelTypes) 79 | } yield 80 | (cc: CC) => cc.copy(content = cc.content.+((cleanAddedField(nextLevelName), s"List[${nextLevelTypeName}]"))), 81 | jObj => { 82 | for { 83 | currentState <- get[T] 84 | ccModifications <- jObj.toMap.toList.traverse { 85 | case (key, value) => 86 | val safeNextLevelName = findFreeName(currentState.doneCCs, key) 87 | buildCC(value, safeNextLevelName) 88 | } 89 | updatedCC = ccModifications.foldLeft(CC(nextLevelName, Map.empty))((cc, ccOp) => ccOp(cc)) 90 | _ <- modify[T](t => t.copy(doneCCs = updatedCC :: t.doneCCs)) 91 | } yield 92 | (cc: CC) => 93 | cc.copy(content = cc.content.+((cleanAddedField(nextLevelName), nextLevelName))) // ignored in first run 94 | } 95 | ) 96 | 97 | @scala.annotation.tailrec 98 | private def findFreeName(ccs: List[CC], name: String): String = 99 | if (ccs.exists(_.name == name)) findFreeName(ccs, name + "1") else name 100 | 101 | private def cleanAddedField(nextLevelName: String): String = nextLevelName.reverse.dropWhile(_ == '1').reverse 102 | } 103 | -------------------------------------------------------------------------------- /src/it/scala/io/yannick_cw/sjq/ExecuteAccessPatternTest.scala: -------------------------------------------------------------------------------- 1 | package io.yannick_cw.sjq 2 | 3 | import io.circe.parser.parse 4 | import org.scalatest.{FlatSpec, Matchers} 5 | import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks 6 | import io.circe._ 7 | import io.circe.testing.instances._ 8 | 9 | class ExecuteAccessPatternTest extends FlatSpec with Matchers with ScalaCheckDrivenPropertyChecks { 10 | 11 | it should "work for json" in { 12 | val testJson = """{ "subclass": { "ids": [22, 23, 24] }}""" 13 | 14 | (for { 15 | res <- executeAccessPattern("root", testJson) 16 | expected <- parse(testJson) 17 | parsedRes <- parse(res) 18 | } yield parsedRes shouldBe expected).fold(throw _, x => x) 19 | } 20 | 21 | it should "work for complex json" in { 22 | (for { 23 | res <- executeAccessPattern("root", complexJson) 24 | expected <- parse(complexJson) 25 | parsedRes <- parse(res) 26 | } yield parsedRes shouldBe expected).fold(throw _, x => x) 27 | } 28 | 29 | ignore should "work for all json" in { 30 | forAll { json: Json => 31 | whenever(json.isObject) { 32 | (for { 33 | res <- executeAccessPattern("root", json.spaces2) 34 | expected = json 35 | parsedRes <- parse(res) 36 | } yield parsedRes shouldBe expected).fold(throw _, x => x) 37 | } 38 | } 39 | } 40 | 41 | val complexJson = """ 42 | |{ 43 | |"query": "mall", 44 | |"hotels": { 45 | |"count": 9305, 46 | |"position": 2, 47 | |"entities": [ 48 | |{ 49 | |"id": "bcbd353a-5e08-4ffc-b84a-86aac88568ef", 50 | |"name": "Close to Wiregrass mall & outlet mall", 51 | |"classifier": "HOTEL", 52 | |"highlighted": "Close to Wiregrass mall & outlet mall", 53 | |"score": 660.220703125, 54 | |"parents": [ 55 | |"Wesley Chapel", 56 | |"Florida", 57 | |"USA" 58 | |], 59 | |"holidayRegions": [ 60 | |"Florida Westküste", 61 | |"Golf von Mexiko", 62 | |"Florida Küste", 63 | |"USA Ostküste" 64 | |], 65 | |"rankingScore": 1, 66 | |"recommendationRate": 0, 67 | |"alternativeNames": [], 68 | |"placeDetailString": "Hotel in Wesley Chapel, Florida, USA", 69 | |"highlightedName": "Close to Wiregrass mall & outlet mall", 70 | |"highlightedParents": [], 71 | |"highlightedPlaceDetailString": "" 72 | |} 73 | |] 74 | |}, 75 | |"destinations": { 76 | |"count": 33, 77 | |"position": 1, 78 | |"entities": [ 79 | |{ 80 | |"id": "07f5f656-4acc-3230-b7dd-aec3c13af37c", 81 | |"name": "Mallorca", 82 | |"classifier": "DESTINATION_REGION", 83 | |"highlighted": "Mallorca", 84 | |"score": 1009.79931640625, 85 | |"parents": [ 86 | |"Spanien" 87 | |], 88 | |"holidayRegions": [], 89 | |"rankingScore": 526309, 90 | |"recommendationRate": null, 91 | |"alternativeNames": [], 92 | |"placeDetailString": "Region in Spanien", 93 | |"highlightedName": "Mallorca", 94 | |"highlightedParents": [], 95 | |"highlightedPlaceDetailString": "" 96 | |}, 97 | |{ 98 | |"id": "67fb4605-9516-3cf9-8d02-53d87086c57a", 99 | |"name": "Malles Venosta / Mals", 100 | |"classifier": "DESTINATION_CITY", 101 | |"highlighted": "Mals", 102 | |"score": 495.9881896972656, 103 | |"parents": [ 104 | |"Südtirol", 105 | |"Italien" 106 | |], 107 | |"holidayRegions": [ 108 | |"Alpen", 109 | |"Skigebiet Reschenpass in Südtirol", 110 | |"Vinschgau" 111 | |], 112 | |"rankingScore": 584, 113 | |"recommendationRate": null, 114 | |"alternativeNames": [], 115 | |"placeDetailString": "Ort in Südtirol, Italien", 116 | |"highlightedName": "Mals", 117 | |"highlightedParents": [], 118 | |"highlightedPlaceDetailString": "" 119 | |}, 120 | |{ 121 | |"id": "46804445-c792-340b-beb1-003b37b37c36", 122 | |"name": "Mallow", 123 | |"classifier": "DESTINATION_CITY", 124 | |"highlighted": "Mallow", 125 | |"score": 433.5486145019531, 126 | |"parents": [ 127 | |"Munster", 128 | |"Irland" 129 | |], 130 | |"holidayRegions": [], 131 | |"rankingScore": 284, 132 | |"recommendationRate": null, 133 | |"alternativeNames": [], 134 | |"placeDetailString": "Ort in Munster, Irland", 135 | |"highlightedName": "Mallow", 136 | |"highlightedParents": [], 137 | |"highlightedPlaceDetailString": "" 138 | |}, 139 | |{ 140 | |"id": "523f034a-064e-3251-805a-55eca882ce68", 141 | |"name": "Mallnitz", 142 | |"classifier": "DESTINATION_CITY", 143 | |"highlighted": "Mallnitz", 144 | |"score": 360.3058166503906, 145 | |"parents": [ 146 | |"Kärnten", 147 | |"Österreich" 148 | |], 149 | |"holidayRegions": [ 150 | |"Hohe Tauern", 151 | |"Skigebiet Mölltaler Gletscher in Kärnten", 152 | |"Alpen", 153 | |"Pongau", 154 | |"Skigebiet Großarltal - Dorfgastein (Ski Amadé)" 155 | |], 156 | |"rankingScore": 108, 157 | |"recommendationRate": null, 158 | |"alternativeNames": [], 159 | |"placeDetailString": "Ort in Kärnten, Österreich", 160 | |"highlightedName": "Mallnitz", 161 | |"highlightedParents": [], 162 | |"highlightedPlaceDetailString": "" 163 | |}, 164 | |{ 165 | |"id": "b934be2d-e042-3a8e-8fcc-00301dc2fb7d", 166 | |"name": "Palma de Mallorca", 167 | |"classifier": "DESTINATION_CITY", 168 | |"highlighted": "Palma de Mallorca", 169 | |"score": 357.1685791015625, 170 | |"parents": [ 171 | |"Mallorca", 172 | |"Spanien" 173 | |], 174 | |"holidayRegions": [ 175 | |"Balearen" 176 | |], 177 | |"rankingScore": 46650, 178 | |"recommendationRate": null, 179 | |"alternativeNames": [], 180 | |"placeDetailString": "Ort in Mallorca, Spanien", 181 | |"highlightedName": "Palma de Mallorca", 182 | |"highlightedParents": [ 183 | |"Mallorca" 184 | |], 185 | |"highlightedPlaceDetailString": "Ort in Mallorca, Spanien" 186 | |} 187 | |] 188 | |}, 189 | |"bucketPriority": { 190 | |"hotelProbability": 0.00026465528648934763, 191 | |"destinationProbability": 0.9997353447135107 192 | |}, 193 | |"passions": [], 194 | |"cruises": [] 195 | |} 196 | """.stripMargin 197 | } 198 | -------------------------------------------------------------------------------- /src/test/scala/io/yannick_cw/sjq/jsonToCaseClassTest.scala: -------------------------------------------------------------------------------- 1 | package io.yannick_cw.sjq 2 | 3 | import org.scalatest.{FlatSpec, Matchers} 4 | import io.circe.parser.parse 5 | 6 | class jsonToCaseClassTest extends FlatSpec with Matchers { 7 | 8 | it should "parse json" in { 9 | val json = parse(""" 10 | | { 11 | | "name": "theName" 12 | | } 13 | """.stripMargin).fold(throw _, x => x) 14 | 15 | jsonToCaseClass(json) shouldBe List(("CC", "case class CC(name: String)")) 16 | } 17 | 18 | it should "parse json with arrays" in { 19 | val json = parse(""" 20 | | { 21 | | "names": ["theName", "otherName"] 22 | | } 23 | """.stripMargin).fold(throw _, x => x) 24 | 25 | jsonToCaseClass(json) shouldBe List(("CC", "case class CC(names: List[String])")) 26 | } 27 | 28 | it should "parse json with number arrays" in { 29 | val json = parse(""" 30 | | { 31 | | "ids": [22, 23] 32 | | } 33 | """.stripMargin).fold(throw _, x => x) 34 | 35 | jsonToCaseClass(json) shouldBe List(("CC", "case class CC(ids: List[Double])")) 36 | } 37 | 38 | it should "parse json with nested json objects" in { 39 | val json = parse(""" 40 | | { 41 | | "name": { "first": "Jo", "nr": 22 } 42 | | } 43 | """.stripMargin).fold(throw _, x => x) 44 | 45 | jsonToCaseClass(json) shouldBe List(("CC", "case class CC(name: name)"), 46 | ("name", "case class name(first: String, nr: Double)")) 47 | } 48 | 49 | it should "parse json with double nested json objects" in { 50 | val json = parse(""" 51 | | { 52 | | "name": { "first": "Jo", "info": { "age": 22, "more": "more" } } 53 | | } 54 | """.stripMargin).fold(throw _, x => x) 55 | 56 | jsonToCaseClass(json) shouldBe List(("CC", "case class CC(name: name)"), 57 | ("name", "case class name(first: String, info: info)"), 58 | ("info", "case class info(age: Double, more: String)")) 59 | } 60 | 61 | it should "parse json with null values" in { 62 | val json = parse(""" 63 | | { 64 | | "name": null, 65 | | "id": 22 66 | | } 67 | """.stripMargin).fold(throw _, x => x) 68 | 69 | jsonToCaseClass(json) shouldBe List(("CC", "case class CC(name: Null, id: Double)")) 70 | } 71 | 72 | it should "parse json with null values and not null values for a field" in { 73 | val json = parse(""" 74 | | { 75 | | "things": [ 76 | | { "id": null }, 77 | | { "id": 22, "more": { "name": "Xx" } } 78 | | ] 79 | | } 80 | """.stripMargin).fold(throw _, x => x) 81 | 82 | jsonToCaseClass(json) shouldBe List( 83 | ("CC", "case class CC(things: List[things])"), 84 | ("things", "case class things(more: Option[more], id: Option[Double])"), 85 | ("more", "case class more(name: String)") 86 | ) 87 | } 88 | 89 | it should "parse json empty lists" in { 90 | val json = parse(""" 91 | | { 92 | | "names": [] 93 | | } 94 | """.stripMargin).fold(throw _, x => x) 95 | 96 | jsonToCaseClass(json) shouldBe List(("CC", "case class CC(names: List[String])")) 97 | } 98 | 99 | it should "parse json with nested objects in arrays" in { 100 | val json = 101 | parse(""" 102 | |{ 103 | | "hotels": { 104 | | "entities": [ 105 | | { "id": "1aa4c4ad-f9ea-3367-a163-8a3a6884d450", "name": "Dana Beach Resort", "ids": [1,2,3] } 106 | | ] 107 | | } 108 | |}""".stripMargin) 109 | .fold(throw _, x => x) 110 | 111 | jsonToCaseClass(json) shouldBe List( 112 | ("CC", "case class CC(hotels: hotels)"), 113 | ("hotels", "case class hotels(entities: List[entities])"), 114 | ("entities", "case class entities(name: String, ids: List[Double], id: String)") 115 | ) 116 | } 117 | 118 | it should "parse json with different types in objects in arrays making them optional" in { 119 | val json = parse(""" 120 | | { 121 | | "list": [ { "id": 22, "other": 12 }, { "name": "Toben", "other": 12} ] 122 | | } 123 | """.stripMargin).fold(throw _, x => x) 124 | 125 | jsonToCaseClass(json) shouldBe List( 126 | ("CC", "case class CC(list: List[list])"), 127 | ("list", "case class list(other: Double, name: Option[String], id: Option[Double])")) 128 | } 129 | 130 | it should "parse json with different types in objects in arrays making only the always optional ones optional" in { 131 | val json = parse(""" 132 | | { 133 | | "list": [ { "id": 22 }, { "name": "Toben", "id": 12 } ] 134 | | } 135 | """.stripMargin).fold(throw _, x => x) 136 | 137 | jsonToCaseClass(json) shouldBe List(("CC", "case class CC(list: List[list])"), 138 | ("list", "case class list(name: Option[String], id: Double)")) 139 | } 140 | 141 | it should "parse json with the same structure twice" in { 142 | val json = parse(""" 143 | | { 144 | | "sub": { "double": { "xx": "sds" } }, 145 | | "another": { "double": { "xx": "sds" } } 146 | | } 147 | """.stripMargin).fold(throw _, x => x) 148 | 149 | jsonToCaseClass(json) shouldBe List( 150 | ("CC", "case class CC(sub: sub, another: another)"), 151 | ("another", "case class another(double: double1)"), 152 | ("double1", "case class double1(xx: String)"), 153 | ("sub", "case class sub(double: double)"), 154 | ("double", "case class double(xx: String)"), 155 | ) 156 | } 157 | 158 | it should "parse json with the same name but different structures" in { 159 | val json = parse(""" 160 | | { 161 | | "sub": { "double": { "xx": 22 } }, 162 | | "another": { "double": { "xx": "sds" } } 163 | | } 164 | """.stripMargin).fold(throw _, x => x) 165 | 166 | jsonToCaseClass(json) shouldBe List( 167 | ("CC", "case class CC(sub: sub, another: another)"), 168 | ("another", "case class another(double: double1)"), 169 | ("double1", "case class double1(xx: String)"), 170 | ("sub", "case class sub(double: double)"), 171 | ("double", "case class double(xx: Double)"), 172 | ) 173 | } 174 | 175 | //todo 176 | ignore should "work for top level jsons" in { 177 | val json = parse("22").fold(throw _, x => x) 178 | 179 | jsonToCaseClass(json) shouldBe List( 180 | ("CC", "Double") 181 | ) 182 | } 183 | 184 | it should "work for different types in an array" in { 185 | val json = parse(""" 186 | | { 187 | | "list": [ 22, "Hi", false ] 188 | | } 189 | """.stripMargin).fold(throw _, x => x) 190 | 191 | jsonToCaseClass(json) shouldBe List( 192 | ("CC", "case class CC(list: List[Json])") 193 | ) 194 | } 195 | } 196 | --------------------------------------------------------------------------------