8

Assuming the following json payload

val json = """{
  "choices" : [
    {
      "name" : "A"
    },
    {
      "name" : "B"
    },
    {
      "name" : "C"
    },
    {
      "name" : "D"
    }
  ],
  "domain" : "Quizz",
  "level" : "Test",
  "mandatory": true
}"""

How do I transform it to

val json = """{
  "value":"B",
  "domain" : "Quizz",
  "level" : "Test",
}"""

where the "B" is randomly selected from the available choices ?

This is what I have got so far :

val cursor = parse(json).getOrElse(Json.Null).cursor
for{
  noMandatory<- cursor.downField("mandatory").flatMap(_.delete).map(_.top)
  withEmptyValue = noMandatory.deepMerge(Json.obj("value"->Json.Null))
 }yield withEmptyValue

This drops the unused "mandatory" field and inserts an empty "value" field. Getting a random value from the array and placing it in "value" escapes me though.

-- edit

I have tried using hcursor which makes the above clearer (to me anyway)

val cursor = parse(json).getOrElse(Json.Null).hcursor

val noMandatory = cursor.downField("mandatory").delete
val withEmptyValue = noMandatory.withFocus(_.deepMerge(Json.obj("value"->Json.Null)))

(I am using circe 0.5.1 in the above examples)

Jean
  • 21,329
  • 5
  • 46
  • 64

3 Answers3

3

Do not mix side effects with pure code. So I didn't solve your randomisation problem.

Second, I'd recommend not removing the 'choices' field to keep things simple. Remove that field later when you feel like it.

Third, separate the getting and transformation stages for simplicity. Here you can use a case class to do most of the work.

I'm sure this might not be the idiomatic Circe solution but it's good Scala:

import io.circe._, io.circe.generic.auto._, io.circe.parser._, io.circe.syntax._

object ChooserApp extends App {
  val input =
    """{
  "choices" : [
    {
      "name" : "A"
    },
    {
      "name" : "B"
    },
    {
      "name" : "C"
    },
    {
      "name" : "D"
    }
  ],
  "domain" : "Quizz",
  "level" : "Test",
  "mandatory": true
}"""

  val expected =
    """{
      "choices" : [
          {
            "name" : "A"
          },
          {
            "name" : "B"
          },
          {
            "name" : "C"
          },
          {
            "name" : "D"
          }
        ],
  "value":"B",
  "domain" : "Quizz",
  "level" : "Test",
  "mandatory": true
}"""

  case class ForJson(j: Json) {
    def choices: List[String] = {
      j.asObject
        .toList
        .flatMap(_ ("choices"))
        .flatMap(_.asArray)
        .flatten
        .flatMap(_.asObject)
        .flatMap(_ ("name"))
        .flatMap(_.asString)
    }

    def chosen(a: String): Json = {
      j.asObject
        .map(_.add("value", Json.fromString(a)))
        .map(Json.fromJsonObject)
        .getOrElse(j)
    }
  }

  val expectedList = List("A", "B", "C", "D")
  val gotList = ForJson(parse(input).toOption.get).choices
  assert(gotList == expectedList, s"Expected $expectedList, got $gotList")

  val expectedJson = parse(expected).toOption.get
  val gotJson = ForJson(parse(input).toOption.get).chosen("B")
  assert(gotJson == expectedJson, s"Expected $expectedJson, got $gotJson")
}
ScalaWilliam
  • 733
  • 4
  • 16
2

my 2cents

@  for{
       j <- parser.parse(json).toOption
       jObj <- j.asObject
       jChoices <- jObj("choices")
       choices <- jChoices.as[Seq[Map[String,Json]]].toOption
       v <- choices(Random.nextInt(choices.size)).get("name")
    } yield {
       jObj.add("value", v ) .remove("choices")
    }   

res58: Option[io.circe.JsonObject] = Some(object[domain -> "Quizz",level -> "Test",mandatory -> true,value -> "C"])
WeiChing 林煒清
  • 4,452
  • 3
  • 30
  • 65
  • This looks really nice. I don't have the project code at hand and maybe this has become possible with api changes in the last 2 years but I'll upvote it for now :) – Jean Mar 22 '18 at 08:32
1

After digging through the api here is what I came up with :

// a couple utilities
def renameKey(srcKey:String,target:String )(obj:JsonObject):Json= {
  obj("name").fold(obj)(value => obj.add("value", value).remove("name")).asJson
}
def safeDropKeys(json:Json)(keys:String*):Json=
  json.withObject(obj=>keys.foldLeft(obj)((acc, s)=>acc.remove(s)).asJson)

The random extractor which I separated as per @scala-william advice:

def extractRandomChip(cursor: HCursor): Xor[DecodingFailure, Json] = {
  val choice = cursor.get[Array[Json]]("choices").map { choices =>
    val x = util.Random.nextInt(choices.length)
    choices(x).withObject(renameKey("name", "value"))
  }
  choice
}

Then gluing it all together :

 val json = for {
  initial <- parse(s)
  value <- extractRandomChip(initial.hcursor)
  cleanedUp = safeDropKeys(initial)("mandatory","choices")
} yield cleanedUp.deepMerge(value)
json.map(_.toString).toOption

As per my comment on @wheaties answer, I'll benchmark both options and accept the results of the fastest option.

Jean
  • 21,329
  • 5
  • 46
  • 64