├── 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 |
--------------------------------------------------------------------------------