aboutsummaryrefslogblamecommitdiff
path: root/shared/src/main/scala/DerivedFormats.scala
blob: ccf48d15bfff5015c1df9b244e9c2fb6cbefc0e7 (plain) (tree)
1
2
3
4
5
6
7
8
9
                  
 
                 


                                         
                                                                 
                                                         
                                            

                                   

                                                                               
                                                                     
 








                                                                                  

                                                                
                                               





                                                                                  
         

                            
 

                                                          



                                       
                                                           
                                                  
                                                       
                      
                                     

                                              
             
           

                               

                                                               

       
 

                                                                     
                     


                                       
 



                                                                          
                     
                                                                    

                                     

                       

       








                                                                     

                                                               
         
 




                                                                   

                                                             
         
       
     
   
 

                                              

 

                                                                     
                                                                     
 



                                                                           

                                                                             


                                  
 







                                                                         
                            




                                                                                   
                           










                                                                 
package spray.json

import magnolia._

import scala.language.experimental.macros

/** Mixin that enables derivation of JSON formats for any product
  * (case classes) or coproduct (sealed traits) types. */
trait DerivedFormats { self: BasicFormats =>
  type Typeclass[T] = JsonFormat[T]

  /** Convert the name of a parameter to that of a field in a JSON object. This
    * method can be overriden to use alternative naming conventions. */
  @inline def extractFieldName(paramName: String): String = paramName

  /** Determines if `None` instances of options should be included in JSON output.
    *
    * By default, `None` values are ommitted entirely from resulting JSON
    * objects. If overridden, they will be included as `null`s instead.
    *
    * Note that this has no effect in *reading* option types; undefined or null
    * values are always converted to `None`. */
  def printNull: Boolean = false

  def combine[T](ctx: CaseClass[JsonFormat, T]): JsonFormat[T] =
    new JsonFormat[T] {
      override def write(value: T): JsValue = {
        val fields: Seq[(String, JsValue)] = ctx.parameters.collect {
          case param
              if !param.option || param.dereference(value) != None || printNull =>
            extractFieldName(param.label) -> param.typeclass.write(
              param.dereference(value)
            )
        }
        JsObject(fields: _*)
      }

      override def read(value: JsValue): T = value match {
        case obj: JsObject =>
          if (ctx.isObject) {
            ctx.rawConstruct(Seq.empty)
          } else {
            ctx.construct { param =>
              val fieldName = extractFieldName(param.label)
              val fieldValue = if (param.option) {
                obj.fields.getOrElse(fieldName, JsNull)
              } else {
                obj.fields(fieldName)
              }
              param.typeclass.read(fieldValue)
            }
          }
        case js =>
          deserializationError(
            s"Cannot read JSON '$js' as a ${ctx.typeName.full}"
          )
      }
    }

  def dispatch[T](ctx: SealedTrait[JsonFormat, T]): JsonFormat[T] = {
    val typeFieldName = ctx.annotations
      .collectFirst {
        case g: gadt => g.typeFieldName
      }
      .getOrElse("type")

    new JsonFormat[T] {
      override def write(value: T): JsValue = ctx.dispatch(value) { sub =>
        sub.typeclass.write(sub.cast(value)) match {
          case obj: JsObject =>
            JsObject(
              (Map(typeFieldName -> JsString(sub.typeName.short)) ++
                obj.fields).toSeq: _*
            )
          case js => js
        }
      }

      override def read(js: JsValue): T = {
        val typeName: String = js match {
          case obj: JsObject if obj.fields.contains(typeFieldName) =>
            obj.fields(typeFieldName).convertTo[String]
          case JsString(str) =>
            str
          case _ =>
            deserializationError(
              s"Cannot deserialize JSON to ${ctx.typeName.full} " +
                "because serialized type cannot be determined."
            )
        }

        ctx.subtypes.find(_.typeName.short == typeName) match {
          case Some(tpe) => tpe.typeclass.read(js)
          case None =>
            deserializationError(
              s"Cannot deserialize JSON to ${ctx.typeName.full} " +
                s"because type '${typeName}' is unsupported."
            )
        }
      }
    }
  }

  def jsonFormat[T]: RootJsonFormat[T] =
    macro DerivedFormatMacros.derivedFormat[T]

}

@deprecated("use DerivedJsonProtocol", "spray-json-derivation 0.4.3")
object DerivedFormats extends DerivedFormats with DefaultJsonProtocol

trait ImplicitDerivedFormats extends DerivedFormats { self: BasicFormats =>
  implicit def implicitJsonFormat[T]: RootJsonFormat[T] =
    macro DerivedFormatMacros.derivedFormat[T]
}

@deprecated("use ImplicitDerivedJsonProtocol", "spray-json-derivation 0.4.3")
object ImplicitDerivedFormats
    extends ImplicitDerivedFormats
    with DefaultJsonProtocol

trait DerivedJsonProtocol extends DefaultJsonProtocol with DerivedFormats
object DerivedJsonProtocol extends DerivedJsonProtocol

trait ImplicitDerivedJsonProtocol
    extends DefaultJsonProtocol
    with ImplicitDerivedFormats
object ImplicitDerivedJsonProtocol extends ImplicitDerivedJsonProtocol

object DerivedFormatMacros {
  import scala.reflect.macros.whitebox._

  /** Utility that converts a magnolia-generated JsonFormat to a RootJsonFormat. */
  def derivedFormat[T: c.WeakTypeTag](c: Context): c.Tree = {
    import c.universe._
    val tpe = weakTypeOf[T]
    val sprayPkg = c.mirror.staticPackage("spray.json")
    val valName = TermName(c.freshName("format"))
    q"""{
      val $valName = ${Magnolia.gen[T](c)}
      new $sprayPkg.RootJsonFormat[$tpe] {
        def write(value: $tpe) = $valName.write(value)
        def read(value: $sprayPkg.JsValue) = $valName.read(value)
      }
    }"""
  }
}