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

org.coursera.naptime.courier.CourierSerializer.scala Maven / Gradle / Ivy

There is a newer version: 0.9.0-alpha5
Show newest version
/*
 * Copyright 2016 Coursera Inc.
 *
 * 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 org.coursera.naptime.courier

import com.linkedin.data.DataMap
import com.linkedin.data.codec.JacksonDataCodec
import com.linkedin.data.codec.TextDataCodec
import com.linkedin.data.schema.DataSchema
import com.linkedin.data.schema.TyperefDataSchema
import com.linkedin.data.schema.validation.ValidateDataAgainstSchema
import com.linkedin.data.schema.validation.ValidationOptions
import com.linkedin.data.schema.validation.ValidationResult
import com.linkedin.data.schema.validator.DataSchemaAnnotationValidator
import com.linkedin.data.template.DataTemplate
import com.linkedin.data.template.UnionTemplate
import org.coursera.courier.templates.DataTemplates.DataConversion
import org.coursera.courier.templates.DataValidationException
import org.coursera.pegasus.TypedDefinitionCodec

import scala.reflect.ClassTag

/**
 * Provides methods for serializing and deserializing the Pegasus data types used by Courier
 * to JSON.
 *
 * This uses [[TypedDefinitionCodec]], the default codec for use with Courier at Coursera.
 *
 * For example, given a generated Courier data binding class named `Profile`, to serialize the
 * Courier data binding class (a.k.a. data template) to JSON:
 *
 * ```
 * val profile = Profile(...)
 * val jsonString = CourierSerializer.write(profile)
 * ```
 *
 * And to Deserialize JSON to the Courier data binding:
 *
 * ```
 * val profile = CourierSerializer.read[Profile](jsonString)
 * ```
 */
object CourierSerializer {

  /**
   * Reads a record template from a JSON string.
   *
   * @throws DataValidationException if validation fails.
   */
  def read[T <: DataTemplate[DataMap]](json: String)(implicit tag: ClassTag[T]): T = {
    val clazz = tag.runtimeClass.asInstanceOf[Class[T]]
    val dataMap = readDataMap(json, tag.runtimeClass.asInstanceOf[Class[T]])
    CourierSerializer.builder(clazz).validateAndBuild(dataMap) match {
      case Right(template) => template
      case Left(validationResult) => throw new DataValidationException(validationResult)
    }
  }

  def write[T <: DataTemplate[DataMap]](template: T)(implicit tag: ClassTag[T]): String = {
    writeDataMap(template.data(), tag.runtimeClass.asInstanceOf[Class[T]])
  }

  /**
   * Reads a union template from a JSON string.
   *
   * @throws DataValidationException if validation fails.
   */
  def readUnion[T <: UnionTemplate](json: String)(implicit tag: ClassTag[T]): T = {
    val clazz = tag.runtimeClass.asInstanceOf[Class[T]]
    val dataMap = readUnion(json, clazz)
    CourierSerializer.builder(clazz).validateAndBuild(dataMap) match {
      case Right(template) => template
      case Left(validationResult) => throw new DataValidationException(validationResult)
    }
  }

  def writeUnion[T <: UnionTemplate](template: T)(implicit tag: ClassTag[T]): String = {
    template.data() match {
      case dataMap: DataMap =>
        writeUnion(dataMap, tag.runtimeClass.asInstanceOf[Class[T]])
      case _: AnyRef =>
        throw new IllegalArgumentException("Null union values not supported by CourierSerializers")
    }
  }

  private[this] val recordValidationOptions = new ValidationOptions()

  class TemplateBuilder[T <: DataTemplate[_ <: AnyRef]](private val clazz: Class[T]) {
    private[this] val companionInstance = companion(clazz)
    private[this] val schema = getSchema(clazz)
    private[this] val annotationValidator = new DataSchemaAnnotationValidator(schema)

    private[this] val applyMethod = {
      companionInstance.getClass.getDeclaredMethod(
        "apply",
        classOf[DataMap],
        classOf[DataConversion])
    }

    def build(dataMap: DataMap): T = {
      applyMethod.invoke(companionInstance, dataMap, DataConversion.SetReadOnly).asInstanceOf[T]
    }

    def validateAndBuild(dataMap: DataMap): Either[ValidationResult, T] = {
      val validationResult =
        ValidateDataAgainstSchema.validate(
          dataMap, schema, recordValidationOptions, annotationValidator)
      if (!validationResult.isValid) {
        Left(validationResult)
      } else {
        Right(
          applyMethod.invoke(
            companionInstance, dataMap, DataConversion.SetReadOnly).asInstanceOf[T])
      }
    }
  }

  def builder[T <: DataTemplate[_ <: AnyRef]](clazz: Class[T]): TemplateBuilder[T] = {
    new TemplateBuilder(clazz)
  }

  def getSchema[T <: DataTemplate[_]](implicit clazz: Class[T]): DataSchema = {
    getSchema(clazz, schemaFieldName)
  }

  /**
   * For unions declared in a typeref, gets the typeref schema.
   */
  def getDeclaringTyperefSchema[T <: DataTemplate[_]](
      implicit clazz: Class[T]): Option[TyperefDataSchema] = {
    try {
      getSchema(clazz, typerefSchemaFieldName) match {
        case schema: TyperefDataSchema => Some(schema)
        case unknown: DataSchema =>
          throw new IllegalStateException(
            s"$typerefSchemaFieldName must be a TyperefDataSchema but found $unknown")
      }
    } catch {
      case e: NoSuchMethodException => None
    }
  }

  private[this] def getSchema[T <: DataTemplate[_]](
      clazz: Class[T], fieldName: String): DataSchema = {
    val companionInstance = companion(clazz)
    val companionClass = companionInstance.getClass
    companionClass.getDeclaredMethod(fieldName).invoke(companionInstance).asInstanceOf[DataSchema]
  }

  private[this] val schemaFieldName = "SCHEMA"
  private[this] val typerefSchemaFieldName = "TYPEREF_SCHEMA"

  private[this] def companion(clazz: Class[_]): AnyRef = {
    import scala.reflect.runtime.universe

    val mirror = universe.runtimeMirror(clazz.getClassLoader)
    val classSymbol = mirror.classSymbol(clazz)
    val companionMirror = mirror.reflectModule(classSymbol.companion.asModule)
    companionMirror.instance.asInstanceOf[AnyRef]
  }

  private[this] val underlyingCodec = new JacksonDataCodec()

  private[this] def readDataMap[T <: DataTemplate[DataMap]](
      json: String, clazz: Class[T]): DataMap = {
    codec(clazz).stringToMap(json)
  }

  private[this] def readUnion[T <: UnionTemplate](
      json: String, clazz: Class[T]): DataMap = {
    codec(clazz).stringToMap(json)
  }

  private[this] def writeDataMap[T <: DataTemplate[DataMap]](
      dataMap: DataMap, clazz: Class[T]): String = {
    codec(clazz).mapToString(dataMap)
  }

  private[this] def writeUnion[T <: DataTemplate[AnyRef]](
      dataMap: DataMap, clazz: Class[T]): String = {
    codec(clazz).mapToString(dataMap)
  }

  private[this] def codec[T <: DataTemplate[_]](clazz: Class[T]): TextDataCodec = {
    codec(CourierSerializer.getSchema(clazz))
  }

  private[this] def codec[T <: DataTemplate[_]](schema: DataSchema): TextDataCodec = {
    new TypedDefinitionCodec(schema, underlyingCodec)
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy