diff --git a/project/Build.scala b/project/Build.scala index 098aad11..ea7793b0 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -46,7 +46,7 @@ object Courier extends Build with OverridablePublishSettings { // In order to keep it under control we primarily concern ourselves with these two below Scala // version numbers: - lazy val sbtScalaVersion = "2.10.5" // the version of Scala used by the current sbt version. + lazy val sbtScalaVersion = "2.11.6" // the version of Scala used by the current sbt version. lazy val currentScalaVersion = "2.11.6" // The current scala version. // Our plugin runs as part of SBT so must use the same Scala version that SBT currently uses. diff --git a/reference-suite/src/main/courier/org/coursera/records/test/WithPrimitivesSimple.courier b/reference-suite/src/main/courier/org/coursera/records/test/WithPrimitivesSimple.courier new file mode 100644 index 00000000..b7c34f64 --- /dev/null +++ b/reference-suite/src/main/courier/org/coursera/records/test/WithPrimitivesSimple.courier @@ -0,0 +1,12 @@ +namespace org.coursera.records.test + +@backend="simple" +record WithPrimitivesSimple { + intField: int + longField: long + floatField: float + doubleField: double + booleanField: boolean + stringField: string + bytesField: bytes +} \ No newline at end of file diff --git a/scala/generator-test/src/test/scala/org/coursera/courier/generator/SimpleBackendTest.scala b/scala/generator-test/src/test/scala/org/coursera/courier/generator/SimpleBackendTest.scala new file mode 100644 index 00000000..38a475f8 --- /dev/null +++ b/scala/generator-test/src/test/scala/org/coursera/courier/generator/SimpleBackendTest.scala @@ -0,0 +1,32 @@ +package org.coursera.courier.generator + +import org.coursera.records.test.WithPrimitives +import org.coursera.records.test.WithPrimitivesSimple +import org.junit.Test +import org.scalatest.junit.AssertionsForJUnit +import org.scalatest.junit.JUnitSuite +import play.api.libs.json.Json + +class SimpleBackendTest extends JUnitSuite with AssertionsForJUnit { + + @Test + def schemasMatch(): Unit = { + // Courier uses `string` instead of `String` so we homogenize here + val old = Json.parse( + WithPrimitives.SCHEMA.toString + .replaceAll("\"string\"", "\"String\"") + .replaceAll("\"bytes\"", "\"ByteString\"") + .replaceAll("\"boolean\"", "\"Boolean\"") + .replaceAll("\"int\"", "\"Int\"") + .replaceAll("\"long\"", "\"Long\"") + .replaceAll("\"float\"", "\"Float\"") + .replaceAll("\"double\"", "\"Double\"") + ) + val `new` = WithPrimitivesSimple.SCHEMA + // We skip `name` because those are necessarily different + assert(`new` \ "type" === old \ "type") + assert(`new` \ "namespace" === old \ "namespace") + assert(`new` \ "fields" === old \ "fields") + } + +} diff --git a/scala/generator/src/main/twirl/org/coursera/courier/templates/RecordClass.scala.txt b/scala/generator/src/main/twirl/org/coursera/courier/templates/RecordClass.scala.txt index 4cc3a8bd..5c2f180c 100644 --- a/scala/generator/src/main/twirl/org/coursera/courier/templates/RecordClass.scala.txt +++ b/scala/generator/src/main/twirl/org/coursera/courier/templates/RecordClass.scala.txt @@ -4,217 +4,242 @@ @import com.linkedin.data.schema.JsonBuilder @import org.coursera.courier.generator.specs._ -@if(record.isTopLevel) { - @record.namespace.map { namespace => package @namespace } - - import javax.annotation.Generated - - import com.linkedin.data.DataMap - import com.linkedin.data.schema.DataSchema - import com.linkedin.data.schema.UnionDataSchema - import com.linkedin.data.schema.TyperefDataSchema - import com.linkedin.data.template.Custom - import com.linkedin.data.template.DataTemplate - import com.linkedin.data.template.RecordTemplate - import com.linkedin.data.template.RequiredFieldNotPresentException - import com.linkedin.data.template.TemplateOutputCastException - import com.linkedin.data.template.UnionTemplate - import org.coursera.courier.templates.DataTemplates - import org.coursera.courier.templates.DataTemplates.DataConversion - import org.coursera.courier.templates.ScalaRecordTemplate - import org.coursera.courier.templates.ScalaArrayTemplate - import org.coursera.courier.templates.ScalaUnionTemplate - import org.coursera.courier.templates.ScalaEnumTemplate - import org.coursera.courier.templates.ScalaEnumTemplateSymbol - import org.coursera.courier.companions.UnionCompanion - import org.coursera.courier.companions.UnionMemberCompanion - import org.coursera.courier.companions.UnionWithTyperefCompanion - import org.coursera.courier.companions.RecordCompanion - import org.coursera.courier.companions.ArrayCompanion - import org.coursera.courier.companions.MapCompanion - import scala.runtime.ScalaRunTime - import com.linkedin.data.template.DataTemplateUtil - import com.linkedin.data.schema.RecordDataSchema - import com.linkedin.data.schema.UnionDataSchema - import com.linkedin.data.schema.TyperefDataSchema - import com.linkedin.data.schema.DataSchemaConstants - import com.linkedin.data.ByteString - import com.linkedin.data.DataList - import scala.collection.JavaConverters._ - import scala.collection.immutable.Iterable - import scala.collection.immutable.MapLike - import scala.collection.mutable.Builder - import scala.collection.generic.CanBuildFrom - import com.linkedin.data.schema.MapDataSchema - import com.linkedin.data.schema.ArrayDataSchema - import scala.collection.GenTraversable - import org.coursera.courier.codecs.InlineStringCodec - import org.coursera.courier.coercers.SingleElementCaseClassCoercer - import scala.language.implicitConversions -} +@if(record.properties.get("backend").exists(_ == "simple")) { -@ClassAnnotations(record) final class @record.scalaType private (private val dataMap: DataMap) - extends ScalaRecordTemplate(dataMap, @(record.scalaType).SCHEMA) with Product { - @(record.scalaType) // force static initialization - import @(record.scalaType)._ - - @* Provide read access to all fields. *@ - @record.fields.map { field => - @* TODO(jbetz): Simplify obtain methods, see ScalaRecordTemplate notes. *@ - @field.typ match { - case primitiveField: PrimitiveDefinition => { - @FieldAnnotations(field) lazy val @(field.name): @(field.scalaType) = @field.wrapIfOption{obtainDirect(@(record.scalaType).Fields.@(field.name), classOf[@(primitiveField.dataType)])} - } - case recordField: RecordDefinition => { - @FieldAnnotations(field) lazy val @(field.name): @(field.scalaTypeFullname) = @field.wrapIfOption{obtainWrapped(@(record.scalaType).Fields.@(field.name), classOf[@(recordField.scalaTypeFullname)])} - } - case unionField: UnionDefinition => { - @FieldAnnotations(field) lazy val @(field.name): @(field.scalaTypeFullname) = @field.wrapAndMapIfOption { - dataMap.getDataMap(@(record.scalaType).Fields.@(field.name).getName) - } { value => @(unionField.scalaTypeFullname).build(@value, DataConversion.SetReadOnly) } - } - case arrayField: ArrayDefinition => { - @FieldAnnotations(field) lazy val @(field.name): @(field.scalaTypeFullname) = @field.wrapIfOption{obtainWrapped(@(record.scalaType).Fields.@(field.name), classOf[@(arrayField.scalaTypeFullname)])} - } - case mapField: MapDefinition => { - @FieldAnnotations(field) lazy val @(field.name): @(field.scalaTypeFullname) = @field.wrapIfOption{obtainWrapped(@(record.scalaType).Fields.@(field.name), classOf[@(mapField.scalaTypeFullname)])} - } - case enumField: EnumDefinition => { - @FieldAnnotations(field) lazy val @(field.name): @(field.scalaTypeFullname) = @field.wrapAndMapIfOption { - obtainDirect(@(record.scalaType).Fields.@(field.name), classOf[String]) - } { value => @(enumField.enumFullname).withName(@value) } - } - case customField: ClassDefinition => { - @FieldAnnotations(field) lazy val @(field.name): @(field.scalaTypeFullname) = @field.wrapIfOption{obtainCustomType(@(record.scalaType).Fields.@(field.name), classOf[@(customField.scalaTypeFullname)])} - } - case fixedField: FixedDefinition => { - @FieldAnnotations(field) lazy val @(field.name): @(field.scalaTypeFullname) = @field.wrapIfOption{obtainWrapped(@(record.scalaType).Fields.@(field.name), classOf[@(fixedField.scalaTypeFullname)])} + @if(record.isTopLevel) { + @record.namespace.map { namespace => package @namespace } + + import com.linkedin.data.ByteString + import org.coursera.courier.schema.Schema + } + + case class @record.scalaType ( + @if(!record.fields.isEmpty) { + @*Trailing comma after all fields except last*@ + @record.fields.init.map { field => + @(field.name): @(field.scalaType), } - } + @(record.fields.last.name): @(record.fields.last.scalaType) + } else {} + ) + object @(record.scalaType) { + val SCHEMA = Schema[@record.scalaType].asJson + } + +} else { + + @if(record.isTopLevel) { + @record.namespace.map { namespace => package @namespace } + + import javax.annotation.Generated + + import com.linkedin.data.DataMap + import com.linkedin.data.schema.DataSchema + import com.linkedin.data.schema.UnionDataSchema + import com.linkedin.data.schema.TyperefDataSchema + import com.linkedin.data.template.Custom + import com.linkedin.data.template.DataTemplate + import com.linkedin.data.template.RecordTemplate + import com.linkedin.data.template.RequiredFieldNotPresentException + import com.linkedin.data.template.TemplateOutputCastException + import com.linkedin.data.template.UnionTemplate + import org.coursera.courier.templates.DataTemplates + import org.coursera.courier.templates.DataTemplates.DataConversion + import org.coursera.courier.templates.ScalaRecordTemplate + import org.coursera.courier.templates.ScalaArrayTemplate + import org.coursera.courier.templates.ScalaUnionTemplate + import org.coursera.courier.templates.ScalaEnumTemplate + import org.coursera.courier.templates.ScalaEnumTemplateSymbol + import org.coursera.courier.companions.UnionCompanion + import org.coursera.courier.companions.UnionMemberCompanion + import org.coursera.courier.companions.UnionWithTyperefCompanion + import org.coursera.courier.companions.RecordCompanion + import org.coursera.courier.companions.ArrayCompanion + import org.coursera.courier.companions.MapCompanion + import scala.runtime.ScalaRunTime + import com.linkedin.data.template.DataTemplateUtil + import com.linkedin.data.schema.RecordDataSchema + import com.linkedin.data.schema.UnionDataSchema + import com.linkedin.data.schema.TyperefDataSchema + import com.linkedin.data.schema.DataSchemaConstants + import com.linkedin.data.ByteString + import com.linkedin.data.DataList + import scala.collection.JavaConverters._ + import scala.collection.immutable.Iterable + import scala.collection.immutable.MapLike + import scala.collection.mutable.Builder + import scala.collection.generic.CanBuildFrom + import com.linkedin.data.schema.MapDataSchema + import com.linkedin.data.schema.ArrayDataSchema + import scala.collection.GenTraversable + import org.coursera.courier.codecs.InlineStringCodec + import org.coursera.courier.coercers.SingleElementCaseClassCoercer + import scala.language.implicitConversions } - @* Set all fields. Only called during initialization. *@ - private def setFields(@(record.fieldParamDefs)): Unit = { + @ClassAnnotations(record) final class @record.scalaType private (private val dataMap: DataMap) + extends ScalaRecordTemplate(dataMap, @(record.scalaType).SCHEMA) with Product { + @(record.scalaType) // force static initialization + import @(record.scalaType)._ + + @* Provide read access to all fields. *@ @record.fields.map { field => - @* TODO(jbetz): Simplify put methods, see ScalaRecordTemplate notes. *@ + @* TODO(jbetz): Simplify obtain methods, see ScalaRecordTemplate notes. *@ @field.typ match { case primitiveField: PrimitiveDefinition => { - @field.applyIfOption(field.name) { value => putDirect(@(record.scalaType).Fields.@(field.name), classOf[@(primitiveField.dataType)], @primitiveField.maybeBox{@value})} + @FieldAnnotations(field) lazy val @(field.name): @(field.scalaType) = @field.wrapIfOption{obtainDirect(@(record.scalaType).Fields.@(field.name), classOf[@(primitiveField.dataType)])} } case recordField: RecordDefinition => { - @field.applyIfOption(field.name) { value => putWrapped(@(record.scalaType).Fields.@(field.name), classOf[@(recordField.scalaTypeFullname)], @value)} + @FieldAnnotations(field) lazy val @(field.name): @(field.scalaTypeFullname) = @field.wrapIfOption{obtainWrapped(@(record.scalaType).Fields.@(field.name), classOf[@(recordField.scalaTypeFullname)])} } case unionField: UnionDefinition => { - @field.applyIfOption(field.name) { value => dataMap.put(@(record.scalaType).Fields.@(field.name).getName, @(value).data())} + @FieldAnnotations(field) lazy val @(field.name): @(field.scalaTypeFullname) = @field.wrapAndMapIfOption { + dataMap.getDataMap(@(record.scalaType).Fields.@(field.name).getName) + } { value => @(unionField.scalaTypeFullname).build(@value, DataConversion.SetReadOnly) } } case arrayField: ArrayDefinition => { - @field.applyIfOption(field.name) { value => putWrapped(@(record.scalaType).Fields.@(field.name), classOf[@(arrayField.scalaTypeFullname)], @value)} + @FieldAnnotations(field) lazy val @(field.name): @(field.scalaTypeFullname) = @field.wrapIfOption{obtainWrapped(@(record.scalaType).Fields.@(field.name), classOf[@(arrayField.scalaTypeFullname)])} } case mapField: MapDefinition => { - @field.applyIfOption(field.name) { value => putWrapped(@(record.scalaType).Fields.@(field.name), classOf[@(mapField.scalaTypeFullname)], @value)} + @FieldAnnotations(field) lazy val @(field.name): @(field.scalaTypeFullname) = @field.wrapIfOption{obtainWrapped(@(record.scalaType).Fields.@(field.name), classOf[@(mapField.scalaTypeFullname)])} } case enumField: EnumDefinition => { - @field.applyIfOption(field.name) { value => putDirect(@(record.scalaType).Fields.@(field.name), classOf[String], @(value).toString)} + @FieldAnnotations(field) lazy val @(field.name): @(field.scalaTypeFullname) = @field.wrapAndMapIfOption { + obtainDirect(@(record.scalaType).Fields.@(field.name), classOf[String]) + } { value => @(enumField.enumFullname).withName(@value) } } case customField: ClassDefinition => { - @field.applyIfOption(field.name) { value => putCustomType(@(record.scalaType).Fields.@(field.name), classOf[@(customField.scalaTypeFullname)], classOf[@(field.dataClass.rawDataType)], @customField.maybeBox{@value})} + @FieldAnnotations(field) lazy val @(field.name): @(field.scalaTypeFullname) = @field.wrapIfOption{obtainCustomType(@(record.scalaType).Fields.@(field.name), classOf[@(customField.scalaTypeFullname)])} } case fixedField: FixedDefinition => { - @field.applyIfOption(field.name) { value => putWrapped(@(record.scalaType).Fields.@(field.name), classOf[@(fixedField.scalaTypeFullname)], @value)} + @FieldAnnotations(field) lazy val @(field.name): @(field.scalaTypeFullname) = @field.wrapIfOption{obtainWrapped(@(record.scalaType).Fields.@(field.name), classOf[@(fixedField.scalaTypeFullname)])} } } } - } - override val productArity: Int = @(record.fields.size) - - override def productElement(n: Int): Any = - n match { - @record.fields.zipWithIndex.map { case (field, i) => - case @i => @field.name} - case _ => throw new IndexOutOfBoundsException(n.toString) + @* Set all fields. Only called during initialization. *@ + private def setFields(@(record.fieldParamDefs)): Unit = { + @record.fields.map { field => + @* TODO(jbetz): Simplify put methods, see ScalaRecordTemplate notes. *@ + @field.typ match { + case primitiveField: PrimitiveDefinition => { + @field.applyIfOption(field.name) { value => putDirect(@(record.scalaType).Fields.@(field.name), classOf[@(primitiveField.dataType)], @primitiveField.maybeBox{@value})} + } + case recordField: RecordDefinition => { + @field.applyIfOption(field.name) { value => putWrapped(@(record.scalaType).Fields.@(field.name), classOf[@(recordField.scalaTypeFullname)], @value)} + } + case unionField: UnionDefinition => { + @field.applyIfOption(field.name) { value => dataMap.put(@(record.scalaType).Fields.@(field.name).getName, @(value).data())} + } + case arrayField: ArrayDefinition => { + @field.applyIfOption(field.name) { value => putWrapped(@(record.scalaType).Fields.@(field.name), classOf[@(arrayField.scalaTypeFullname)], @value)} + } + case mapField: MapDefinition => { + @field.applyIfOption(field.name) { value => putWrapped(@(record.scalaType).Fields.@(field.name), classOf[@(mapField.scalaTypeFullname)], @value)} + } + case enumField: EnumDefinition => { + @field.applyIfOption(field.name) { value => putDirect(@(record.scalaType).Fields.@(field.name), classOf[String], @(value).toString)} + } + case customField: ClassDefinition => { + @field.applyIfOption(field.name) { value => putCustomType(@(record.scalaType).Fields.@(field.name), classOf[@(customField.scalaTypeFullname)], classOf[@(field.dataClass.rawDataType)], @customField.maybeBox{@value})} + } + case fixedField: FixedDefinition => { + @field.applyIfOption(field.name) { value => putWrapped(@(record.scalaType).Fields.@(field.name), classOf[@(fixedField.scalaTypeFullname)], @value)} + } + } + } } - override val productPrefix: String = "@(record.scalaType)" + override val productArity: Int = @(record.fields.size) - override def canEqual(that: Any): Boolean = that.isInstanceOf[@(record.scalaType)] + override def productElement(n: Int): Any = + n match { + @record.fields.zipWithIndex.map { case (field, i) => + case @i => @field.name} + case _ => throw new IndexOutOfBoundsException(n.toString) + } - override def hashCode: Int = ScalaRunTime._hashCode(this) + override val productPrefix: String = "@(record.scalaType)" - override def equals(that: Any): Boolean = canEqual(that) && ScalaRunTime._equals(this, that) + override def canEqual(that: Any): Boolean = that.isInstanceOf[@(record.scalaType)] - override def toString: String = ScalaRunTime._toString(this) + override def hashCode: Int = ScalaRunTime._hashCode(this) - override def copy(): @(record.scalaType) = this + override def equals(that: Any): Boolean = canEqual(that) && ScalaRunTime._equals(this, that) - @if(record.fields.nonEmpty) { - def copy(@(record.copyFieldParamDefs)): @(record.scalaType) = { - val $dataMap = new DataMap - val $result = new @(record.scalaType)($dataMap) - $result.setFields(@(record.fieldsAsParams)) - $dataMap.makeReadOnly() - $result + override def toString: String = ScalaRunTime._toString(this) + + override def copy(): @(record.scalaType) = this + + @if(record.fields.nonEmpty) { + def copy(@(record.copyFieldParamDefs)): @(record.scalaType) = { + val $dataMap = new DataMap + val $result = new @(record.scalaType)($dataMap) + $result.setFields(@(record.fieldsAsParams)) + $dataMap.makeReadOnly() + $result + } } - } - override def copy(dataMap: DataMap, conversion: DataConversion): @(record.scalaType) = { - new @(record.scalaType)(DataTemplates.makeImmutable(dataMap, conversion)) - } + override def copy(dataMap: DataMap, conversion: DataConversion): @(record.scalaType) = { + new @(record.scalaType)(DataTemplates.makeImmutable(dataMap, conversion)) + } - override def clone(): @(record.scalaType) = this -} + override def clone(): @(record.scalaType) = this + } -object @(record.scalaType) extends RecordCompanion[@(record.scalaType)] { - val SCHEMA = DataTemplateUtil.parseSchema(@("\"\"\"" + SchemaToJsonEncoder.schemaToJson(record.recordSchema, JsonBuilder.Pretty.COMPACT) + "\"\"\"")).asInstanceOf[RecordDataSchema] + object @(record.scalaType) extends RecordCompanion[@(record.scalaType)] { + val SCHEMA = DataTemplateUtil.parseSchema(@("\"\"\"" + SchemaToJsonEncoder.schemaToJson(record.recordSchema, JsonBuilder.Pretty.COMPACT) + "\"\"\"")).asInstanceOf[RecordDataSchema] - implicit val @(record.implicitCompanionName): RecordCompanion[@(record.scalaType)] = this + implicit val @(record.implicitCompanionName): RecordCompanion[@(record.scalaType)] = this - @* Register custom types and coercers. *@ - @record.customInfosToRegister.map { case (field, customInfos) => - @customInfos.map { customInfo => - @registerCustomInfo(customInfo, field.dataClass) + @* Register custom types and coercers. *@ + @record.customInfosToRegister.map { case (field, customInfos) => + @customInfos.map { customInfo => + @registerCustomInfo(customInfo, field.dataClass) + } } - } - - @* Generate any contained types as inner classes. *@ - @ContainedTypes(record) - private object Fields { - @record.fields.map { field => - val @(field.name) = @(record.scalaType).SCHEMA.getField("@(field.pegasusName)")} - } + @* Generate any contained types as inner classes. *@ + @ContainedTypes(record) - @* TODO(jbetz): Include defaults, if field is optional, provide way to default it to either none or a value. *@ - def apply(@(record.fieldParamDefs)): @(record.scalaType) = { - val $dataMap = new DataMap - val $result = new @(record.scalaType)($dataMap) - $result.setFields(@(record.fieldsAsParams)) - $dataMap.makeReadOnly() - $result - } + private object Fields { + @record.fields.map { field => + val @(field.name) = @(record.scalaType).SCHEMA.getField("@(field.pegasusName)")} + } - override def build(dataMap: DataMap, conversion: DataConversion): @(record.scalaType) = { - new @(record.scalaType)(DataTemplates.makeImmutable(dataMap, conversion)) - } + @* TODO(jbetz): Include defaults, if field is optional, provide way to default it to either none or a value. *@ + def apply(@(record.fieldParamDefs)): @(record.scalaType) = { + val $dataMap = new DataMap + val $result = new @(record.scalaType)($dataMap) + $result.setFields(@(record.fieldsAsParams)) + $dataMap.makeReadOnly() + $result + } - @* Scala, why you make unapply so complicated? *@ - @record.fields.size match { - case 0 => { - def unapply(record: @(record.scalaType)): Boolean = true + override def build(dataMap: DataMap, conversion: DataConversion): @(record.scalaType) = { + new @(record.scalaType)(DataTemplates.makeImmutable(dataMap, conversion)) } - case i if i <= 22 => { @* Scala tuples only exist up to Tuple22. *@ - def unapply(record: @(record.scalaType)): Option[(@(record.fieldsAsTypeParams))] = { - try { - Some((@(record.prefixedFieldParams("record.")))) - } catch { - case cast: TemplateOutputCastException => None - case notPresent: RequiredFieldNotPresentException => None + + @* Scala, why you make unapply so complicated? *@ + @record.fields.size match { + case 0 => { + def unapply(record: @(record.scalaType)): Boolean = true + } + case i if i <= 22 => { @* Scala tuples only exist up to Tuple22. *@ + def unapply(record: @(record.scalaType)): Option[(@(record.fieldsAsTypeParams))] = { + try { + Some((@(record.prefixedFieldParams("record.")))) + } catch { + case cast: TemplateOutputCastException => None + case notPresent: RequiredFieldNotPresentException => None + } } } - } - case _ => { - /* unapply() not available for records with more than 22 fields due to Scala's Tuple limit. */ + case _ => { + /* unapply() not available for records with more than 22 fields due to Scala's Tuple limit. */ + } } } -} +} diff --git a/scala/runtime/build.sbt b/scala/runtime/build.sbt index e153e6ed..6a394e6b 100644 --- a/scala/runtime/build.sbt +++ b/scala/runtime/build.sbt @@ -3,6 +3,8 @@ name := "courier-runtime" runtimeVersionSettings libraryDependencies ++= Seq( + "com.typesafe.play" %% "play-json" % "2.6.2", + "com.chuusai" %% "shapeless" % "2.3.2", ExternalDependencies.Pegasus.data, ExternalDependencies.Coursera.courscala, ExternalDependencies.JUnit.junit, diff --git a/scala/runtime/src/main/scala/org/coursera/courier/schema/FieldTypes.scala b/scala/runtime/src/main/scala/org/coursera/courier/schema/FieldTypes.scala new file mode 100644 index 00000000..3140351a --- /dev/null +++ b/scala/runtime/src/main/scala/org/coursera/courier/schema/FieldTypes.scala @@ -0,0 +1,31 @@ +package org.coursera.courier.schema + +import shapeless.DepFn0 +import shapeless.HList +import shapeless.HNil +import shapeless.labelled.FieldType + +import scala.reflect.ClassTag + +trait FieldTypes[L <: HList] extends DepFn0 { type Out <: HList } + +object FieldTypes { + type Aux[L <: HList, Out0 <: HList] = FieldTypes[L] { type Out = Out0 } + + def apply[L <: HList](implicit fieldTypes: FieldTypes[L]): Aux[L, fieldTypes.Out] = fieldTypes + + implicit def hnilFieldTypes[L <: HNil]: Aux[L, HNil] = new FieldTypes[L] { + type Out = HNil + def apply(): Out = HNil + } + type ::[+A, +B <: shapeless.HList] = shapeless.::[A, B] + + implicit def hlistFieldTypes[K, V, Rest <: HList]( + implicit fieldTypesRest: FieldTypes[Rest], + clazz: ClassTag[V] + ): Aux[FieldType[K, V] :: Rest, String :: fieldTypesRest.Out] = new FieldTypes[FieldType[K, V] :: Rest] { + type Out = String :: fieldTypesRest.Out + + def apply(): Out = clazz.runtimeClass.getName :: fieldTypesRest() + } +} diff --git a/scala/runtime/src/main/scala/org/coursera/courier/schema/Schema.scala b/scala/runtime/src/main/scala/org/coursera/courier/schema/Schema.scala new file mode 100644 index 00000000..b37ce530 --- /dev/null +++ b/scala/runtime/src/main/scala/org/coursera/courier/schema/Schema.scala @@ -0,0 +1,57 @@ +package org.coursera.courier.schema + +import play.api.libs.json.JsArray +import play.api.libs.json.JsObject +import play.api.libs.json.Json +import shapeless.HList +import shapeless.LabelledGeneric +import shapeless.Poly1 +import shapeless.Typeable +import shapeless.ops.hlist.LiftAll +import shapeless.ops.hlist.Mapper +import shapeless.ops.hlist.ToTraversable +import shapeless.ops.record.UnzipFields + +import scala.reflect.ClassTag + +class Schema[A] { + def asJson[ + Repr <: HList, + Keys <: HList, + Values <: HList, + Typeables <: HList, + MapperOut <: HList + ](implicit + clazz: ClassTag[A], + generic: LabelledGeneric.Aux[A, Repr], + unzip: UnzipFields.Aux[Repr, Keys, Values], + typeable: LiftAll.Aux[Typeable, Values, Typeables], + fieldTypes: Mapper.Aux[Schema.describe.type, Typeables, MapperOut], + traversable1: ToTraversable.Aux[Keys, List, Symbol], + traversable2: ToTraversable.Aux[MapperOut, List, String] + ): JsObject = { + val keys = unzip.keys.toList.map(_.name) + val types: List[String] = fieldTypes(typeable.instances).toList + val fields = keys.zip(types).map { + case (name, typ) => + Json.obj( + "name" -> name, + "type" -> typ + ) + } + Json.obj( + "type" -> "record", + "name" -> clazz.runtimeClass.getSimpleName, + "namespace" -> clazz.runtimeClass.getPackage.getName, + "fields" -> JsArray(fields) + ) + } +} + +object Schema { + def apply[A] = new Schema[A] + + object describe extends Poly1 { + implicit def typeable[T] = at[Typeable[T]](_.describe) + } +} diff --git a/scala/test-lib/target/scala-2.11/courier/org/coursera/records/test/WithPrimitives.scala b/scala/test-lib/target/scala-2.11/courier/org/coursera/records/test/WithPrimitives.scala new file mode 100644 index 00000000..1c29a092 --- /dev/null +++ b/scala/test-lib/target/scala-2.11/courier/org/coursera/records/test/WithPrimitives.scala @@ -0,0 +1,155 @@ +package org.coursera.records.test + +import javax.annotation.Generated + +import com.linkedin.data.DataMap +import com.linkedin.data.schema.DataSchema +import com.linkedin.data.schema.UnionDataSchema +import com.linkedin.data.schema.TyperefDataSchema +import com.linkedin.data.template.Custom +import com.linkedin.data.template.DataTemplate +import com.linkedin.data.template.RecordTemplate +import com.linkedin.data.template.RequiredFieldNotPresentException +import com.linkedin.data.template.TemplateOutputCastException +import com.linkedin.data.template.UnionTemplate +import org.coursera.courier.templates.DataTemplates +import org.coursera.courier.templates.DataTemplates.DataConversion +import org.coursera.courier.templates.ScalaRecordTemplate +import org.coursera.courier.templates.ScalaArrayTemplate +import org.coursera.courier.templates.ScalaUnionTemplate +import org.coursera.courier.templates.ScalaEnumTemplate +import org.coursera.courier.templates.ScalaEnumTemplateSymbol +import org.coursera.courier.companions.UnionCompanion +import org.coursera.courier.companions.UnionMemberCompanion +import org.coursera.courier.companions.UnionWithTyperefCompanion +import org.coursera.courier.companions.RecordCompanion +import org.coursera.courier.companions.ArrayCompanion +import org.coursera.courier.companions.MapCompanion +import scala.runtime.ScalaRunTime +import com.linkedin.data.template.DataTemplateUtil +import com.linkedin.data.schema.RecordDataSchema +import com.linkedin.data.schema.UnionDataSchema +import com.linkedin.data.schema.TyperefDataSchema +import com.linkedin.data.schema.DataSchemaConstants +import com.linkedin.data.ByteString +import com.linkedin.data.DataList +import scala.collection.JavaConverters._ +import scala.collection.immutable.Iterable +import scala.collection.immutable.MapLike +import scala.collection.mutable.Builder +import scala.collection.generic.CanBuildFrom +import com.linkedin.data.schema.MapDataSchema +import com.linkedin.data.schema.ArrayDataSchema +import scala.collection.GenTraversable +import org.coursera.courier.codecs.InlineStringCodec +import org.coursera.courier.coercers.SingleElementCaseClassCoercer +import scala.language.implicitConversions + +@Generated(value = Array("WithPrimitives"), comments = "Courier Data Template.") +final class WithPrimitives private (private val dataMap: DataMap) + extends ScalaRecordTemplate(dataMap, WithPrimitives.SCHEMA) with Product { + WithPrimitives // force static initialization + import WithPrimitives._ + + lazy val intField: Int = obtainDirect(WithPrimitives.Fields.intField, classOf[java.lang.Integer]) + + lazy val longField: Long = obtainDirect(WithPrimitives.Fields.longField, classOf[java.lang.Long]) + + lazy val floatField: Float = obtainDirect(WithPrimitives.Fields.floatField, classOf[java.lang.Float]) + + lazy val doubleField: Double = obtainDirect(WithPrimitives.Fields.doubleField, classOf[java.lang.Double]) + + lazy val booleanField: Boolean = obtainDirect(WithPrimitives.Fields.booleanField, classOf[java.lang.Boolean]) + + lazy val stringField: String = obtainDirect(WithPrimitives.Fields.stringField, classOf[java.lang.String]) + + lazy val bytesField: ByteString = obtainDirect(WithPrimitives.Fields.bytesField, classOf[com.linkedin.data.ByteString]) + + private def setFields(intField: Int, longField: Long, floatField: Float, doubleField: Double, booleanField: Boolean, stringField: String, bytesField: ByteString): Unit = { + putDirect(WithPrimitives.Fields.intField, classOf[java.lang.Integer], Int.box(intField)) + putDirect(WithPrimitives.Fields.longField, classOf[java.lang.Long], Long.box(longField)) + putDirect(WithPrimitives.Fields.floatField, classOf[java.lang.Float], Float.box(floatField)) + putDirect(WithPrimitives.Fields.doubleField, classOf[java.lang.Double], Double.box(doubleField)) + putDirect(WithPrimitives.Fields.booleanField, classOf[java.lang.Boolean], Boolean.box(booleanField)) + putDirect(WithPrimitives.Fields.stringField, classOf[java.lang.String], stringField) + putDirect(WithPrimitives.Fields.bytesField, classOf[com.linkedin.data.ByteString], bytesField) + } + + override val productArity: Int = 7 + + override def productElement(n: Int): Any = + n match { + case 0 => intField + case 1 => longField + case 2 => floatField + case 3 => doubleField + case 4 => booleanField + case 5 => stringField + case 6 => bytesField + case _ => throw new IndexOutOfBoundsException(n.toString) + } + + override val productPrefix: String = "WithPrimitives" + + override def canEqual(that: Any): Boolean = that.isInstanceOf[WithPrimitives] + + override def hashCode: Int = ScalaRunTime._hashCode(this) + + override def equals(that: Any): Boolean = canEqual(that) && ScalaRunTime._equals(this, that) + + override def toString: String = ScalaRunTime._toString(this) + + override def copy(): WithPrimitives = this + + def copy(intField: Int = this.intField, longField: Long = this.longField, floatField: Float = this.floatField, doubleField: Double = this.doubleField, booleanField: Boolean = this.booleanField, stringField: String = this.stringField, bytesField: ByteString = this.bytesField): WithPrimitives = { + val $dataMap = new DataMap + val $result = new WithPrimitives($dataMap) + $result.setFields(intField, longField, floatField, doubleField, booleanField, stringField, bytesField) + $dataMap.makeReadOnly() + $result + } + + override def copy(dataMap: DataMap, conversion: DataConversion): WithPrimitives = { + new WithPrimitives(DataTemplates.makeImmutable(dataMap, conversion)) + } + + override def clone(): WithPrimitives = this +} + +object WithPrimitives extends RecordCompanion[WithPrimitives] { + val SCHEMA = DataTemplateUtil.parseSchema("""{"type":"record","name":"WithPrimitives","namespace":"org.coursera.records.test","fields":[{"name":"intField","type":"int"},{"name":"longField","type":"long"},{"name":"floatField","type":"float"},{"name":"doubleField","type":"double"},{"name":"booleanField","type":"boolean"},{"name":"stringField","type":"string"},{"name":"bytesField","type":"bytes"}]}""").asInstanceOf[RecordDataSchema] + + implicit val withPrimitivesCompanion: RecordCompanion[WithPrimitives] = this + + private object Fields { + val intField = WithPrimitives.SCHEMA.getField("intField") + val longField = WithPrimitives.SCHEMA.getField("longField") + val floatField = WithPrimitives.SCHEMA.getField("floatField") + val doubleField = WithPrimitives.SCHEMA.getField("doubleField") + val booleanField = WithPrimitives.SCHEMA.getField("booleanField") + val stringField = WithPrimitives.SCHEMA.getField("stringField") + val bytesField = WithPrimitives.SCHEMA.getField("bytesField") + } + + def apply(intField: Int, longField: Long, floatField: Float, doubleField: Double, booleanField: Boolean, stringField: String, bytesField: ByteString): WithPrimitives = { + val $dataMap = new DataMap + val $result = new WithPrimitives($dataMap) + $result.setFields(intField, longField, floatField, doubleField, booleanField, stringField, bytesField) + $dataMap.makeReadOnly() + $result + } + + override def build(dataMap: DataMap, conversion: DataConversion): WithPrimitives = { + new WithPrimitives(DataTemplates.makeImmutable(dataMap, conversion)) + } + + def unapply(record: WithPrimitives): Option[(Int, Long, Float, Double, Boolean, String, ByteString)] = { + try { + Some((record.intField, record.longField, record.floatField, record.doubleField, record.booleanField, record.stringField, record.bytesField)) + } catch { + case cast: TemplateOutputCastException => None + case notPresent: RequiredFieldNotPresentException => None + } + } + +} diff --git a/scala/test-lib/target/scala-2.11/courier/org/coursera/records/test/WithPrimitivesSimple.scala b/scala/test-lib/target/scala-2.11/courier/org/coursera/records/test/WithPrimitivesSimple.scala new file mode 100644 index 00000000..d37278c0 --- /dev/null +++ b/scala/test-lib/target/scala-2.11/courier/org/coursera/records/test/WithPrimitivesSimple.scala @@ -0,0 +1,25 @@ +package org.coursera.records.test + +import com.linkedin.data.ByteString +import org.coursera.courier.schema.Schema + +case class WithPrimitivesSimple ( + + intField: Int, + + longField: Long, + + floatField: Float, + + doubleField: Double, + + booleanField: Boolean, + + stringField: String, + + bytesField: ByteString + +) +object WithPrimitivesSimple { + val SCHEMA = Schema[WithPrimitivesSimple].asJson +}