All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.greenfossil.commons.json.JsValue.scala Maven / Gradle / Ivy

/*
 * Copyright 2022 Greenfossil Pte Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.greenfossil.commons.json

import com.jayway.jsonpath.{Configuration, JsonPath}
import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider
import org.slf4j.LoggerFactory

import scala.collection.immutable.ArraySeq
import scala.language.{dynamics, implicitConversions}
import scala.util.Try

/**
 * https://www.json.org/json-en.html
 * http://tutorials.jenkov.com/java-json/jackson-jsonnode.html
 * https://www.baeldung.com/jackson-serialize-dates
 * https://github.com/FasterXML/jackson-modules-java8
 */
import java.time.*

private[json] val logger = LoggerFactory.getLogger("commons-json")

type Number = Int | Long | Float | Double | BigDecimal | java.math.BigDecimal

type Temporal = java.util.Date | java.sql.Date | java.sql.Time | java.sql.Timestamp |
  LocalDateTime | LocalDate | LocalTime | Instant | OffsetDateTime | OffsetTime | ZonedDateTime

private def primitiveToJsValue(x: String | Boolean | Number | Temporal | Null | JsValue) : JsValue =
  x match
    case null => JsNull
    case s: String => JsString(s)
    case b: Boolean => JsBoolean(b)
    case n: Number => JsNumber(n)
    case t: Temporal => JsTemporal(t)
    case js: JsValue => js

private def toJsonType(x: Any): JsValue =
  import scala.jdk.CollectionConverters.*
  x.asInstanceOf[Matchable] match
    case null => null
    case x : (String | Boolean |  Number | Temporal | JsValue) => primitiveToJsValue(x)
    case xs: Array[?] => JsArray(xs.toIndexedSeq.map(toJsonType))
    case obj: Map[?, ?] => JsObject(obj.toList.map(tup2 => tup2._1.toString -> toJsonType(tup2._2)))
    case it: Iterable[?] => JsArray(it.map(toJsonType).toList)

    //Java types
    case jobj: java.util.Map[?, ?] => toJsonType(jobj.asScala.toMap)
    case jArr: java.util.List[?] =>
      JsArray(jArr.stream().map(x => toJsonType(x)).toList.asScala.toList)

private def longToInstant(len: Int, value: Long): Instant =
  //If precision is 10 or less, assume it is in seconds
  if len < 11 then Instant.ofEpochSecond(value) else Instant.ofEpochMilli(value)

extension [T <: Temporal](t: T)
  def jsonFormat(format: String): JsTemporal = JsTemporal(t, format)
  def jsonFormat(format: String, zoneId: ZoneId): JsTemporal = JsTemporal(t, format, zoneId)
  def jsonFormat(format: String, zoneId: String): JsTemporal = JsTemporal(t, format, ZoneId.of(zoneId))

extension(zonedDT: ZonedDateTime)
  def toTemporal(tpe: String): Temporal =
    tpe match
      case "Instant" => zonedDT.toInstant
      case "LocalDateTime" => zonedDT.toLocalDateTime
      case "LocalDate" => zonedDT.toLocalDate
      case "LocalTime" => zonedDT.toLocalTime
      case "ZonedDateTime" => zonedDT
      case "OffsetDateTime" => zonedDT.toOffsetDateTime
      case "OffsetTime" => zonedDT.toOffsetDateTime.toOffsetTime
      case "java.util.Date" => java.util.Date.from(zonedDT.toInstant)
      case "java.sql.Date" => java.sql.Date.valueOf(zonedDT.toLocalDate)
      case "java.sql.Time" => java.sql.Time.valueOf(zonedDT.toLocalTime)
      case "java.sql.Timestamp" => java.sql.Timestamp.from(zonedDT.toInstant)

extension(i: Instant)
  def toTemporal(tpe: String):Temporal = i.atZone(ZoneId.systemDefault()).toTemporal(tpe)

extension(localDT: LocalDateTime)
  def toTemporal(tpe: String):Temporal = localDT.atZone(ZoneId.systemDefault()).toTemporal(tpe)

extension(localDate: LocalDate)
  def toTemporal(tpe: String):Temporal = localDate.atStartOfDay(ZoneId.systemDefault()).toTemporal(tpe)

extension(localTime: LocalTime)
  def toTemporal(tpe: String):Temporal = LocalDate.now.atTime(localTime).atZone(ZoneId.systemDefault()).toTemporal(tpe)

extension(offsetTime: OffsetTime)
  def toTemporal(tpe: String):Temporal = LocalDate.now.atTime(offsetTime).atZoneSameInstant(ZoneId.systemDefault()).toTemporal(tpe)

extension(offsetDT: OffsetDateTime)
  def toTemporal(tpe: String):Temporal = offsetDT.atZoneSameInstant(ZoneId.systemDefault()).toTemporal(tpe)

extension(jDate: java.util.Date)
  def toTemporal(tpe: String):Temporal = jDate.toInstant.atZone(ZoneId.systemDefault()).toTemporal(tpe)

extension(bd: BigDecimal)
  def toTemporal(tpe: String):Temporal = longToInstant(bd.precision, bd.longValue).atZone(ZoneId.systemDefault()).toTemporal(tpe)

val EPOCTIME_REGEX = """(\d+)""".r
val LOCALDATE_REGEX = """(\d\d\d\d)-(\d\d)-(\d\d)""".r
val LOCALTIME_REGEX = """(\d\d):(\d\d):(\d\d).*""".r
val LOCALDATETIME_REGEX = """(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d).*""".r
val INSTANT_REGEX = """(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d).*Z""".r
val OFFSETDATETIME_REGEX = """(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)\+(\d\d):(\d\d)""".r
val OFFSETTIME_REGEX = """(\d\d):(\d\d):(\d\d)\+(\d\d):(\d\d)""".r
val ZONEDDATETIME_REGEX = """(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)\+(\d\d):(\d\d)\[(.+)\]""".r

extension(s: String)
  def toTemporal(tpe: String):Temporal  =
    val zdt = s match {
      case EPOCTIME_REGEX(long) => longToInstant(long.length, long.toLong).atZone(ZoneId.systemDefault())
      case INSTANT_REGEX(year, month, day, hh, mm, ss) => Instant.parse(s).atZone(ZoneId.systemDefault())
      case ZONEDDATETIME_REGEX(year, month, day, hh, mm, ss, ohh, omm, zone) => ZonedDateTime.parse(s)
      case OFFSETDATETIME_REGEX(year, month, day, hh, mm, ss, ohh, omm) => OffsetDateTime.parse(s).atZoneSimilarLocal(ZoneId.systemDefault())
      case OFFSETTIME_REGEX(hh, mm, ss, ohh, omm) => OffsetTime.parse(s).atDate(LocalDate.now).atZoneSimilarLocal(ZoneId.systemDefault())
      case LOCALDATETIME_REGEX(year, month, day, hh, mm, ss) => LocalDateTime.parse(s.replaceAll("[Z\\+].*", "")).atZone(ZoneId.systemDefault())
      case LOCALDATE_REGEX(year, month, day) => LocalDate.parse(s).atStartOfDay(ZoneId.systemDefault())
      case LOCALTIME_REGEX(hh, mm, ss) => LocalDate.now.atTime(LocalTime.parse(s.replaceAll("[Z\\+].*", ""))).atZone(ZoneId.systemDefault())
    }
    zdt.toTemporal(tpe)

  def $$: JsValue = Json.parse(s)

import scala.compiletime.*

inline private def valueType[T]: String =
  inline erasedValue[T] match
    case _: Instant => "Instant"
    case _: LocalDateTime => "LocalDateTime"
    case _: LocalDate => "LocalDate"
    case _: LocalTime => "LocalTime"
    case _: OffsetTime => "OffsetTime"
    case _: OffsetDateTime => "OffsetDateTime"
    case _: ZonedDateTime => "ZonedDateTime"
    case _: java.sql.Date => "java.sql.Date"
    case _: java.sql.Time => "java.sql.Time"
    case _: java.sql.Timestamp => "java.sql.Timestamp"
    case _: java.util.Date => "java.util.Date"
    case _: JsTemporal => "JsTemporal"
    case _: JsString => "JsString"
    case _: JsBoolean => "JsBoolean"
    case _: JsNumber => "JsNumber"
    case _: JsObject => "JsObject"
    case _: JsArray => "JsArray"
    case _: JsValue => "JsValue"
    case _: Int => "Int"
    case _: Long => "Long"
    case _: Float => "Float"
    case _: Double => "Double"
    case _: BigDecimal => "BigDecimal"
    case _: Boolean => "Boolean"
    case _: String => "String"
    case _: Seq[t] => "[" + valueType[t]
    case _: Map[String, Any] => "Object" //TODO - to support value of other types apart from Any
    case _: Any => "Any"

object JsValue:

  given Conversion[String | Boolean | Number | Temporal | Null, JsValue] =
    toJsonType(_)

  given Conversion[Seq[?], JsArray] with
    def apply(xs: Seq[?]): JsArray = JsArray(xs.map(toJsonType))

  given Conversion[Set[?], JsArray] with
    def apply(xs: Set[?]): JsArray = JsArray(xs.toSeq.map(toJsonType))

  given Conversion[Option[?], Option[JsValue]] with
    def apply(xs: Option[?]): Option[JsValue] = xs.map(toJsonType)


sealed trait JsValue extends Dynamic:
  type A

  def value: A

  private def jsValueToTemporal(jsValue: JsValue, toType: String): Temporal =
    jsValue match
      case x: JsNumber => x.value.toTemporal(toType)
      case x: JsString => x.value.toTemporal(toType)
      case x: JsTemporal if toType == "Any" => x.value
      case x: JsTemporal =>
        x.value match
          case t: Instant => t.toTemporal(toType)
          case t: LocalDateTime => t.toTemporal(toType)
          case t: LocalDate => t.toTemporal(toType)
          case t: LocalTime => t.toTemporal(toType)
          case t: OffsetTime => t.toTemporal(toType)
          case t: OffsetDateTime => t.toTemporal(toType)
          case t: ZonedDateTime => t.toTemporal(toType)
          case t: java.util.Date => t.toTemporal(toType)
      case JsNull => null
      case unsupported => throw new JsonException(s"Temporal Conversion error for unsupported ${unsupported}")

  private def jsValueToBoolean(jsValue: JsValue, tpe: String): Boolean =
    jsValue match
      case JsString(value) => value.toBooleanOption.getOrElse(false)
      case JsNumber(value) => value > 0
      case JsTemporal(value, format, zoneId) => throw new JsonException(s"Temporal Conversion error for unsupported ${jsValue}")
      case JsBoolean(value) => value
      case JsNull => false
      case JsObject(value) => value.isEmpty
      case JsArray(value) => value.isEmpty
      case JsUndefined(value) => throw new JsonException(s"Undefined value [${value}]")

  private def jsValueToString(jsValue: JsValue, tpe: String): String =
    jsValue match
      case JsString(value) =>  value
      case JsNumber(value) => value.toString
      case JsTemporal(value, format, zoneId) => value.toString
      case JsBoolean(value) => value.toString
      case JsNull => null
      case JsObject(value) => jsValue.stringify
      case JsArray(value) => jsValue.stringify
      case JsUndefined(value) => throw new JsonException(s"Undefined value [${value}]")

  private def jsValueToNumber(jsValue: JsValue, tpe: String): Number =
    jsValue match
      case JsString(value) =>
        tpe match
          case "Int" => value.toInt
          case "Long" => value.toLong
          case "Float" => value.toFloat
          case "Double" => value.toDouble
          case "BigDecimal" => BigDecimal.apply(value)
      case JsNumber(value) =>
        tpe match
          case "Any" => value
          case "Int" => value.toInt
          case "Long" => value.toLong
          case "Float" => value.toFloat
          case "Double" => value.toDouble
          case _ => value
      case JsUndefined(value) => throw new JsonException(s"Undefined value [${value}]")
      case unsupported => throw new JsonException(s"Number conversion error for unsupported ${unsupported}. JsTemporal, JsNull, JsObject, JsArray are not supported")

  private def jsValueToArray(jsValue: JsValue, tpe: String): Seq[Any] =
    jsValue match
      case JsString(s) => Seq(s)
      case JsNumber(bd) => Seq(bd)
      case value : JsTemporal => Seq(jsValueToTemporal(value, ""))
      case JsBoolean(b) => Seq(b)
      case JsNull => Seq.empty
      case obj: JsObject =>
        val fields: Seq[(String, Any)] = obj.fields.flatMap{(key, jsValue) =>
          jsValue match
            case v: JsString => Option(key ->  jsValueToString(v, tpe))
            case v: JsNumber => Option(key -> jsValueToNumber(v, tpe))
            case v: JsTemporal => Option(key -> jsValueToTemporal(v, "Instant"))
            case v: JsBoolean => Option(key -> jsValueToBoolean(v, tpe))
            case JsNull => Option(key -> null)
            case v: JsObject => Option(key -> jsValueToObject(v, tpe))
            case v: JsArray => Option(key -> jsValueToArray(v, tpe))
            case v: JsUndefined => None
        }
        fields
      case JsArray(value) => value.map(v => asValue(v, tpe))
      case JsUndefined(value) => throw new JsonException(s"Undefined value [${value}]")

  private def jsValueToObject(jsValue: JsValue, tpe: String): Map[String, Any] =
    jsValue match
      case JsString(s) => Map("1" -> s)
      case JsNumber(bd) => Map("1" -> bd)
      case value : JsTemporal => Map("1" -> jsValueToTemporal(value, ""))
      case JsBoolean(b) => Map("1" -> b)
      case JsNull => Map.empty
      case obj: JsObject =>
        val fields: Seq[(String, Any)] = obj.fields.flatMap{(key, jsValue) =>
          jsValue match
            case v: JsString => Option(key ->  jsValueToString(v, tpe))
            case v: JsNumber => Option(key -> jsValueToNumber(v, tpe))
            case v: JsTemporal => Option(key -> jsValueToTemporal(v, tpe))
            case v: JsBoolean => Option(key -> jsValueToBoolean(v, tpe))
            case JsNull => Option(key -> null)
            case v: JsObject => Option(key -> jsValueToObject(v, tpe))
            case v: JsArray => Option(key -> jsValueToArray(v, tpe))
            case v: JsUndefined => None
        }
        fields.toMap

      case JsArray(value) =>
        val fields: Seq[(String, Any)] = value.zipWithIndex.flatMap{(jsValue, index) =>
          val key = index.toString
          jsValue match
            case v: JsString => Option(key ->  jsValueToString(v, tpe))
            case v: JsNumber => Option(key -> jsValueToNumber(v, tpe))
            case v: JsTemporal => Option(key -> jsValueToTemporal(v, tpe))
            case v: JsBoolean => Option(key -> jsValueToBoolean(v, tpe))
            case JsNull => Option(key -> null)
            case v: JsObject => Option(key -> jsValueToObject(v, tpe))
            case v: JsArray => Option(key -> jsValueToArray(v, tpe))
            case v: JsUndefined => None
        }
        fields.toMap

      case JsUndefined(value) => throw new JsonException(s"Undefined value [${value}]")

  private def jsValueToAny(x: JsValue, tpe: String): Any =
    x match
      case value: JsString =>  value.value
      case value: JsNumber => value.value
      case value: JsTemporal => value.value
      case value: JsBoolean => value.value
      case JsNull => null
      case value: JsObject => jsValueToObject(value, "Any")
      case value: JsArray => jsValueToArray(value, "Any")
      case JsUndefined(value)=> throw new JsonException(s"Undefined value [${value}]")

  private def jsValueToJsTemporal(jsValue: JsValue, tpe: String) =
    jsValue match
      case JsString(s) => JsTemporal(s.toTemporal("Instant"))
      case JsNumber(bd) => JsTemporal(longToInstant(bd.precision, bd.longValue))
      case value : JsTemporal => value
      case JsNull => JsNull
      case JsUndefined(value) => throw new JsonException(s"Undefined value [${value}]")
      case unsupported => throw new JsonException(s"JsTemporal Conversion error for unsupported ${unsupported}. JsObject and JsArray are not supported")

  private def jsValueToJsString(jsValue: JsValue, tpe: String): JsString =
    jsValue match
      case value: JsString => value
      case JsNumber(value) => JsString(value.toString)
      case JsTemporal(value, format, zoneId) => JsString(value.toString)
      case JsBoolean(value) => JsString(value.toString)
      case JsNull => JsString(null)
      case JsObject(value) => JsString(value.toString())
      case JsArray(value) => JsString(value.toString())
      case JsUndefined(value) => throw new JsonException(s"Undefined value [${value}]")

  private def jsValueToJsBoolean(jsValue: JsValue, tpe: String): JsBoolean =
    jsValue match
      case JsString(value) => JsBoolean(value.toBooleanOption.getOrElse(false))
      case JsNumber(value) => JsBoolean(value > 0)
      case JsTemporal(value, format, zoneId) => JsBoolean(false)
      case value: JsBoolean => value
      case JsNull => JsBoolean(false)
      case JsObject(value) => JsBoolean(value.isEmpty)
      case JsArray(value) => JsBoolean(value.isEmpty)
      case JsUndefined(value) => throw new JsonException(s"Undefined value [${value}]")

  private def jsValueToJsNumber(jsValue: JsValue, tpe: String): JsNumber =
    jsValue match
      case JsString(value) =>  JsNumber(jsValueToNumber(jsValue, tpe))
      case value: JsNumber => value
      case JsBoolean(value) => JsNumber( if value then 1 else 0)
      case JsNull => JsNumber(null)
      case JsUndefined(value) => throw new JsonException(s"Undefined value [${value}]")
      case unsupported => throw new JsonException(s"JsNumber Conversion error unsupported ${unsupported}. JsTemporal, JsObject and JsArray are not supported")

  private def jsValueToJsObject(jsValue: JsValue, tpe: String): JsObject =
    jsValue match
      case value: JsObject => value
      case JsUndefined(value) => throw new JsonException(s"Undefined value [${value}]")
      case unsupported => throw new JsonException(s"JsObject Conversion error unsupported ${unsupported}. Only JsObject is supported")

  private def jsValueToJsArray(x: JsValue, tpe: String): JsArray =
    x match
      case value: JsString => JsArray(value)
      case value: JsNumber => JsArray(value)
      case value: JsTemporal => JsArray(value)
      case value: JsBoolean => JsArray(value)
      case JsNull => JsArray.empty
      case value: JsArray => value
      case JsUndefined(value) => throw new JsonException(s"Undefined value [${value}]")
      case unsupported => throw new JsonException(s"JsArray Conversion error unsupported ${unsupported}. JsObject is not supported")

  def asValue(value: JsValue, toType: String): Any =
    //If T is a Type of JsValue use it T else use getValue.asInstanceOf[T]
    toType match
      case "Instant" =>             jsValueToTemporal(value, toType)
      case "LocalDateTime" =>       jsValueToTemporal(value, toType)
      case "LocalDate" =>           jsValueToTemporal(value, toType)
      case "LocalTime" =>           jsValueToTemporal(value, toType)
      case "OffsetTime" =>          jsValueToTemporal(value, toType)
      case "OffsetDateTime" =>      jsValueToTemporal(value, toType)
      case "ZonedDateTime" =>       jsValueToTemporal(value, toType)
      case "java.sql.Date" =>       jsValueToTemporal(value, toType)
      case "java.sql.Time" =>       jsValueToTemporal(value, toType)
      case "java.sql.Timestamp" =>  jsValueToTemporal(value, toType)
      case "java.util.Date" =>      jsValueToTemporal(value, toType)
      case "Int" =>                 jsValueToNumber(value, toType)
      case "Long" =>                jsValueToNumber(value, toType)
      case "Float" =>               jsValueToNumber(value, toType)
      case "Double" =>              jsValueToNumber(value, toType)
      case "BigDecimal" =>          jsValueToNumber(value, toType)
      case "Boolean" =>             jsValueToBoolean(value, toType)
      case "String" =>              jsValueToString(value, toType)
      case "Object" =>              jsValueToObject(value, toType)
      case "JsTemporal" =>          jsValueToJsTemporal(value, toType)
      case "JsString" =>            jsValueToJsString(value, toType)
      case "JsBoolean" =>           jsValueToJsBoolean(value, toType)
      case "JsNumber" =>            jsValueToJsNumber(value, toType)
      case "JsObject" =>            jsValueToJsObject(value, toType)
      case "JsArray" =>             jsValueToJsArray(value, toType)
      case "JsValue" =>             if !value.isInstanceOf[JsUndefined] then value else throw new JsonException(s"Undefined value [${value}]")
      case "Any" =>                 jsValueToAny(value, toType)
      case seq if seq.startsWith("[") =>
        val array = jsValueToJsArray(value, toType)
        array.value.map(v => asValue(v, seq.drop(1)))
      case unsupportedType => throw new JsonException(s"Conversion error: unsupported-type:${unsupportedType} for value [${value}]")

  inline def as[T]: T = asValue(this, valueType[T]).asInstanceOf[T]

  inline def asOpt[T]: Option[T] = 
    scala.util.Try(this.as[T]).toOption
  
  inline def asNonNullOpt[T]: Option[T] = 
    scala.util.Try(this.as[T]).toOption.filter(_ != null)

  inline def toOption: Option[JsValue] = asOpt[JsValue].filterNot(_.isInstanceOf[JsUndefined])

  import com.fasterxml.jackson.databind.JsonNode

  val _jsonNode: JsonNode = JsonModule.mapper.valueToTree(this)

  export _jsonNode.{asBoolean, asDouble, asInt, asLong, asText,
    binaryValue, booleanValue, decimalValue, floatValue, longValue, intValue,
    isArray, isBigDecimal, isBigInteger, isDouble, isFloat, isInt, isNull, isTextual
  }

  def jsonNodeToJsValue[T <: JsValue](node: JsonNode, clazz: Class[T]): T =
    JsonModule.mapper.treeToValue(node, clazz)

  /**
   *
   * @param path - /path/path - need to have a root forward slash
   * @return
   */
  def at(path: String): JsValue = jsonNodeToJsValue(_jsonNode.at(path), classOf[JsValue])

  def \(childIndex: Int): JsValue = _get(childIndex)

  def \(path: String): JsValue =
    if this.isInstanceOf[JsUndefined] then this
    else jsonNodeToJsValue(_jsonNode.at(s"/$path"), classOf[JsValue])

  private def _get(start: Int, count: Int = 1): JsValue =
    require(count > 0, "count must be positive integer")
    if !this.isInstanceOf[JsArray] then JsUndefined("Node must be an JsArray")
    else
      val length = _jsonNode.size()
      val actualStart = if (start >= 0) start else
        length + start - (if count == 1 then 0 else 1)
      val actualEnd = (actualStart + count).min(length)
      val xs = (actualStart until actualEnd).map{ index =>
        jsonNodeToJsValue(_jsonNode.get(index), classOf[JsValue])
      }
      if xs.nonEmpty && count == 1 then xs.head
      else JsArray( if start >=  0 then xs else xs )


  /**
   * wild card search or recursive nested search for path
   *
   * @param path
   * @return
   */
  def \\(path: String): JsArray =
    JsArray(_nodeTraverse(path, _jsonNode, List(), _ => true))

  /**
   * Extract JsValue using path
   * https://github.com/json-path/JsonPath
   * @param path
   * @return
   */
  val _jacksonConfig =  Configuration.builder()
    .jsonProvider(JacksonJsonNodeJsonProvider())
    .mappingProvider(JacksonMappingProvider())
    .build()

  def extract(path: String): Seq[JsValue] =
    import scala.jdk.CollectionConverters.*
      JsonPath.using(_jacksonConfig).parse(_jsonNode).read(path, classOf[Any]) match
        case jArr: java.util.ArrayList[?] =>
          jArr.stream().map(x => toJsonType(x)).toList.asScala.toList
        case other =>
          List(toJsonType(other))

  def selectDynamic(name: String): JsValue =
    if this.isInstanceOf[JsUndefined] then this
    else
      val _name = name.replaceFirst("^\\$", "")
      jsonNodeToJsValue(_jsonNode.at(s"/$_name"), classOf[JsValue])

  def applyDynamic(name: String)(args: Int*): JsValue =
    args match
      case ArraySeq(index: Int) => selectDynamic(name)._get(index)
      case ArraySeq(index: Int, length: Int)  => selectDynamic(name)._get(index, length)
      case _  => throw IllegalArgumentException("Maximum 2 arguments (index, length)")

  private def _nodeTraverse(name: String, node: JsonNode, acc: Seq[JsValue], innerFieldValidator: JsValue => Boolean): Seq[JsValue] =
    import com.fasterxml.jackson.databind.node.ArrayNode

    import scala.jdk.CollectionConverters.*
    if node.isObject then
      val fieldNames = node.fieldNames().asScala
      fieldNames.foldLeft(acc) { (res, fieldName) =>
        if fieldName != name then res ++ _nodeTraverse(name, node.get(fieldName), acc, innerFieldValidator)
        else
          val jsNode = node.get(name)
          val jsValue = jsonNodeToJsValue(jsNode, classOf[JsValue])
          Try(innerFieldValidator(jsValue))
            .fold(
              ex =>
                logger.warn(s"InnertFieldValidator raised an exception.\nIt is likely the attribute does not exist.\nJsValue: ${jsValue.stringify} ", ex)
                res,
              isTrue =>
                if isTrue then res :+ jsValue
                else res
            )
      }
    else if node.isArray then
      val elems = node.asInstanceOf[ArrayNode].elements().asScala
      elems.foldLeft(acc)((res, e) => res ++ _nodeTraverse(name, e, acc, innerFieldValidator))
    else acc

  @deprecated("use stringify instead")
  def toJson: String = Json.stringify(this)

  def stringify: String = Json.stringify(this)
  
  def prettyPrint: String = Json.prettyPrint(this)

  transparent inline def showPretty: JsValue =
    val s = s"\n--------------\nJson Start\n--------------\n${this.prettyPrint}\n------------\nJson End\n------------\n"
    show(Console.out, s)

  transparent inline def show: JsValue =
    val s = s"\n--------------\nJson Start\n--------------\n${this.stringify}\n------------\nJson End\n------------\n"
    show(Console.out, s)

  transparent inline def show(s: java.io.PrintStream, any: Any): JsValue =
    s.println(any)
    this

  def encodeBase64URL: String = encodeBase64URL("UTF-8")

  def encodeBase64URL(charSet: String): String = encodeBase64URL(charSet, false)

  def encodeBase64URL(charSet: String, withPadding: Boolean): String =
    val encoder = if withPadding then java.util.Base64.getUrlEncoder else java.util.Base64.getUrlEncoder.withoutPadding()
    encoder.encodeToString(stringify.getBytes(charSet))

  /**
   * This is same as toJson or Json.stringfy
   *
   * @return
   */
  final override def toString = stringify

end JsValue


/**
 * JsString
 */
case class JsString(value: String) extends JsValue:
  type A = String

/**
 * JsNumber
 */
object JsNumber:
  def apply(n: Number): JsNumber =
    n match 
      case x: Int => JsNumber(BigDecimal(x))
      case x: Long => JsNumber(BigDecimal(x))
      case x: Float => JsNumber(BigDecimal(x.toString))
      case x: Double => JsNumber(BigDecimal(x))
      case x: BigDecimal => JsNumber(x)
      case x: java.math.BigDecimal => JsNumber(BigDecimal(x))

case class JsNumber(value: BigDecimal) extends JsValue:
  type A = BigDecimal

object JsTemporal:
  def apply(t: Temporal): JsTemporal = JsTemporal(t, "")

  /**
   * This method is used by JsonModule for serialization
   * @param jsTemporal
   * @return
   */
  def toJson(jsTemporal: JsTemporal): String | Long =
    if jsTemporal.format == null || jsTemporal.format.isEmpty 
    then
      jsTemporal.value match 
        case jdate: java.util.Date => jdate.getTime
        case _ => jsTemporal.value.toString
    else
      import java.time.format.DateTimeFormatter
      val dtFormatter = DateTimeFormatter.ofPattern(jsTemporal.format)
      jsTemporal.value match 
        case x: java.util.Date => dtFormatter.withZone(jsTemporal.zoneId).format(x.toInstant)
        case x: Instant => dtFormatter.withZone(jsTemporal.zoneId).format(x)
        case x: LocalDateTime => dtFormatter.format(x)
        case x: LocalDate => dtFormatter.format(x)
        case x: LocalTime => dtFormatter.format(x)
        case x: OffsetDateTime => dtFormatter.format(x)
        case x: OffsetTime => dtFormatter.format(x)
        case x: ZonedDateTime => dtFormatter.format(x)

/**
 * @param value
 * @param format - if not defined, the value will be serialized as with toString with the exception of java.util.Date.
 *               java.util.Date is serialized as Epoch-time milli seconds
 */
case class JsTemporal(value: Temporal, format:String, zoneId: ZoneId = ZoneId.from(ZoneOffset.UTC)) extends JsValue:
  type A = Temporal
  def jsonFormat(format: String):JsTemporal = copy(format = format)

/**
 * JsBoolean
 */
case class JsBoolean(value: Boolean) extends JsValue:
  type A = Boolean
  export value.*

/**
 * JsNull
 */
case object JsNull extends JsValue:
  type A = Null
  val value = null

/**
 * JsObject
 */
import scala.collection.immutable

object JsObject:
  val empty: JsObject = JsObject(immutable.ListMap.empty)

  def apply(fields: Seq[(String, JsValue | String | Boolean | Number | Temporal | Null)]): JsObject =
    val jsFields = fields.map((name, v) => name -> primitiveToJsValue(v))
    new JsObject(immutable.ListMap(jsFields *))


case class JsObject(value: immutable.ListMap[String, JsValue]) extends JsValue:
  type A = immutable.ListMap[String, JsValue]
  export value.{ apply as _, - as _ , keys as _,  *}

  def fields: Seq[(String, JsValue)] = value.toList

  /**
   * 
   * @return - a new JsObject where all fields will not be null. If there is not null values, it would return itself
   */
  def removeNullValues(): JsObject = 
    val nonNullFields = fields.filter(_._2 != JsNull)
    if nonNullFields == fields then this
    else JsObject(nonNullFields)

  def apply(field: String): JsValue = value.getOrElse(field, JsUndefined(s"Field ${field} does not exists"))

  def ++ (otherObj: JsObject): JsObject =
    JsObject(value ++ otherObj.value)

  def + (key:String, jsValue: JsValue): JsObject =
    JsObject(this.fields :+ key ->jsValue)

  def - (key: String) : JsObject =
    JsObject(value.filterNot(entry => entry._1 == key))

  def deepMerge(other: JsObject): JsObject =
    def merge(existingObject: JsObject, otherObject: JsObject): JsObject = {
      val result = existingObject.value ++ otherObject.value.map {
        case (otherKey, otherValue) =>
          val maybeExistingValue = existingObject.value.get(otherKey)

          val newValue = (maybeExistingValue, otherValue) match {
            case (Some(e: JsObject), o: JsObject) =>
              merge(e, o)
            case _ =>
              otherValue
          }
          otherKey ->
            newValue
      }
      JsObject(result)
    }
    merge(this, other)
    
  def deepMergeIfTrue(isTrue: => Boolean)(other: => JsObject): JsObject =
    if isTrue then this.deepMerge(other)
    else this
  
  def deepMergeIfFalse(isTrue: => Boolean)(other: => JsObject): JsObject =
    if isTrue then this
    else this.deepMerge(other)

  def keys: Set[String] = value.keys.toSet

end JsObject

/**
 * JsArray
 */
object JsArray:
  val empty: JsArray = JsArray(Seq.empty)
  
  def apply(head: JsValue, tail: JsValue*): JsArray = 
    (head +: tail) match 
      case (elems: JsArray) +: Nil => elems
      case elems => JsArray(elems)

case class JsArray(value: Seq[JsValue]) extends JsValue:
  type A = Seq[JsValue]
  export value.{head, headOption, isEmpty, nonEmpty, collect, map, filter, exists, foldLeft, tail, take, flatMap, size}

  def ++(otherJsArray: JsArray): JsArray =
    JsArray(value ++ otherJsArray.value)

/**
 * JsUndefined
 * @param value
 */
case class JsUndefined(value: String) extends JsValue:
  type A = String




© 2015 - 2024 Weber Informatics LLC | Privacy Policy