The Path to Generic Endpoints using Shapeless
Scala eXchange, December 2017
Maria Livia Chiorean
Me
@MariaLiviaCh
The Guardian
Content API
Composer
Atom Tools
Content Atoms
- Structured data
- Reusable content with its own lifecycle
- Simultaneous updates
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec eleifend condimentum metus, at pretium orci sagittis et. Cras euismod justo ut tellus venenatis, et fermentum lectus feugiat. Nam pellentesque massa ac nulla semper ultricies. In eleifend tempor cursus.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec eleifend condimentum metus, at pretium orci sagittis et.
Cras euismod justo ut tellus venenatis, et fermentum lectus feugiat. Nam pellentesque massa ac nulla semper ultricies. In eleifend tempor cursus.
Atom Workshop
- Currently we have > 10 types of atoms
- Common components
- Shared services
- Built on Scala, Play and React
Generic API Endpoint
PATCH /api/:atomType/:id/:path
val atomData = MediaAtom(
url = "gu.com",
metadata = Some(Metadata(
commentsEnabled = Some(true),
channelId = Some("happy-friday"))))
val atomData = MediaAtom(
title = "Tech time video",
metadata = Some(Metadata(
commentsEnabled = Some(true),
channelId = Some("Scala"))))
/api/media/some-id/metadata.channelId
body: "Scala"
Atom Workshop Architecture
Thrift models
Backend
Frontend
JSON
Thrift + Scrooge
namespace * contentatom.qanda
namespace java com.gu.contentatom.thrift.atom.qanda
#@namespace scala com.gu.contentatom.thrift.atom.qanda
include "../shared.thrift"
include "storyquestions.thrift"
struct QAndAItem {
1: optional string title
2: required string body
}
struct QAndAAtom {
1: optional string typeLabel
3: optional shared.Image eventImage
4: required QAndAItem item
5: optional storyquestions.Question question
}
/**
* Generated by Scrooge
* version: 4.16.0
* rev: 0201cac9fdd6188248d42da91fd14c87744cc4a5
* built at: 20170421-124523
*/
package com.gu.contentatom.thrift.atom.qanda
import com.twitter.scrooge.{
HasThriftStructCodec3,
LazyTProtocol,
TFieldBlob,
ThriftException,
ThriftStruct,
ThriftStructCodec3,
ThriftStructFieldInfo,
ThriftStructMetaData,
ThriftUtil
}
import org.apache.thrift.protocol._
import org.apache.thrift.transport.{TMemoryBuffer, TTransport}
import java.nio.ByteBuffer
import java.util.Arrays
import scala.collection.immutable.{Map => immutable$Map}
import scala.collection.mutable.Builder
import scala.collection.mutable.{
ArrayBuffer => mutable$ArrayBuffer, Buffer => mutable$Buffer,
HashMap => mutable$HashMap, HashSet => mutable$HashSet}
import scala.collection.{Map, Set}
object QAndAAtom extends ThriftStructCodec3[QAndAAtom] {
private val NoPassthroughFields = immutable$Map.empty[Short, TFieldBlob]
val Struct = new TStruct("QAndAAtom")
val TypeLabelField = new TField("typeLabel", TType.STRING, 1)
val TypeLabelFieldManifest = implicitly[Manifest[String]]
val EventImageField = new TField("eventImage", TType.STRUCT, 3)
val EventImageFieldManifest = implicitly[Manifest[com.gu.contentatom.thrift.Image]]
val ItemField = new TField("item", TType.STRUCT, 4)
val ItemFieldManifest = implicitly[Manifest[com.gu.contentatom.thrift.atom.qanda.QAndAItem]]
val QuestionField = new TField("question", TType.STRUCT, 5)
val QuestionFieldManifest = implicitly[Manifest[com.gu.contentatom.thrift.atom.storyquestions.Question]]
/**
* Field information in declaration order.
*/
lazy val fieldInfos: scala.List[ThriftStructFieldInfo] = scala.List[ThriftStructFieldInfo](
new ThriftStructFieldInfo(
TypeLabelField,
true,
false,
TypeLabelFieldManifest,
_root_.scala.None,
_root_.scala.None,
immutable$Map.empty[String, String],
immutable$Map.empty[String, String],
None
),
new ThriftStructFieldInfo(
EventImageField,
true,
false,
EventImageFieldManifest,
_root_.scala.None,
_root_.scala.None,
immutable$Map.empty[String, String],
immutable$Map.empty[String, String],
None
),
new ThriftStructFieldInfo(
ItemField,
false,
true,
ItemFieldManifest,
_root_.scala.None,
_root_.scala.None,
immutable$Map.empty[String, String],
immutable$Map.empty[String, String],
None
),
new ThriftStructFieldInfo(
QuestionField,
true,
false,
QuestionFieldManifest,
_root_.scala.None,
_root_.scala.None,
immutable$Map.empty[String, String],
immutable$Map.empty[String, String],
None
)
)
lazy val structAnnotations: immutable$Map[String, String] =
immutable$Map.empty[String, String]
/**
* Checks that all required fields are non-null.
*/
def validate(_item: QAndAAtom): Unit = {
if (_item.item == null) throw new TProtocolException("Required field item cannot be null")
}
def withoutPassthroughFields(original: QAndAAtom): QAndAAtom =
new Immutable(
typeLabel =
{
val field = original.typeLabel
field.map { field =>
field
}
},
eventImage =
{
val field = original.eventImage
field.map { field =>
com.gu.contentatom.thrift.Image.withoutPassthroughFields(field)
}
},
item =
{
val field = original.item
com.gu.contentatom.thrift.atom.qanda.QAndAItem.withoutPassthroughFields(field)
},
question =
{
val field = original.question
field.map { field =>
com.gu.contentatom.thrift.atom.storyquestions.Question.withoutPassthroughFields(field)
}
}
)
override def encode(_item: QAndAAtom, _oproto: TProtocol): Unit = {
_item.write(_oproto)
}
private[this] def lazyDecode(_iprot: LazyTProtocol): QAndAAtom = {
var typeLabelOffset: Int = -1
var eventImage: Option[com.gu.contentatom.thrift.Image] = None
var item: com.gu.contentatom.thrift.atom.qanda.QAndAItem = null
var _got_item = false
var question: Option[com.gu.contentatom.thrift.atom.storyquestions.Question] = None
var _passthroughFields: Builder[(Short, TFieldBlob), immutable$Map[Short, TFieldBlob]] = null
var _done = false
val _start_offset = _iprot.offset
_iprot.readStructBegin()
while (!_done) {
val _field = _iprot.readFieldBegin()
if (_field.`type` == TType.STOP) {
_done = true
} else {
_field.id match {
case 1 =>
_field.`type` match {
case TType.STRING =>
typeLabelOffset = _iprot.offsetSkipString
case _actualType =>
val _expectedType = TType.STRING
throw new TProtocolException(
"Received wrong type for field 'typeLabel' (expected=%s, actual=%s).".format(
ttypeToString(_expectedType),
ttypeToString(_actualType)
)
)
}
case 3 =>
_field.`type` match {
case TType.STRUCT =>
eventImage = Some(readEventImageValue(_iprot))
case _actualType =>
val _expectedType = TType.STRUCT
throw new TProtocolException(
"Received wrong type for field 'eventImage' (expected=%s, actual=%s).".format(
ttypeToString(_expectedType),
ttypeToString(_actualType)
)
)
}
case 4 =>
_field.`type` match {
case TType.STRUCT =>
item = readItemValue(_iprot)
_got_item = true
case _actualType =>
val _expectedType = TType.STRUCT
throw new TProtocolException(
"Received wrong type for field 'item' (expected=%s, actual=%s).".format(
ttypeToString(_expectedType),
ttypeToString(_actualType)
)
)
}
case 5 =>
_field.`type` match {
case TType.STRUCT =>
question = Some(readQuestionValue(_iprot))
case _actualType =>
val _expectedType = TType.STRUCT
throw new TProtocolException(
"Received wrong type for field 'question' (expected=%s, actual=%s).".format(
ttypeToString(_expectedType),
ttypeToString(_actualType)
)
)
}
case _ =>
if (_passthroughFields == null)
_passthroughFields = immutable$Map.newBuilder[Short, TFieldBlob]
_passthroughFields += (_field.id -> TFieldBlob.read(_field, _iprot))
}
_iprot.readFieldEnd()
}
}
_iprot.readStructEnd()
if (!_got_item) throw new TProtocolException("Required field 'item' was not found in serialized data for struct QAndAAtom")
new LazyImmutable(
_iprot,
_iprot.buffer,
_start_offset,
_iprot.offset,
typeLabelOffset,
eventImage,
item,
question,
if (_passthroughFields == null)
NoPassthroughFields
else
_passthroughFields.result()
)
}
trait QAndAAtom
extends ThriftStruct
with _root_.scala.Product4[Option[String], Option[com.gu.contentatom.thrift.Image], com.gu.contentatom.thrift.atom.qanda.QAndAItem, Option[com.gu.contentatom.thrift.atom.storyquestions.Question]]
with HasThriftStructCodec3[QAndAAtom]
with java.io.Serializable
{
import QAndAAtom._
def typeLabel: _root_.scala.Option[String]
def eventImage: _root_.scala.Option[com.gu.contentatom.thrift.Image]
def item: com.gu.contentatom.thrift.atom.qanda.QAndAItem
def question: _root_.scala.Option[com.gu.contentatom.thrift.atom.storyquestions.Question]
def _passthroughFields: immutable$Map[Short, TFieldBlob] = immutable$Map.empty
def _1 = typeLabel
def _2 = eventImage
def _3 = item
def _4 = question
def toTuple: _root_.scala.Tuple4[Option[String], Option[com.gu.contentatom.thrift.Image], com.gu.contentatom.thrift.atom.qanda.QAndAItem, Option[com.gu.contentatom.thrift.atom.storyquestions.Question]] = {
(
typeLabel,
eventImage,
item,
question
)
}
/**
* Gets a field value encoded as a binary blob using TCompactProtocol. If the specified field
* is present in the passthrough map, that value is returned. Otherwise, if the specified field
* is known and not optional and set to None, then the field is serialized and returned.
*/
def getFieldBlob(_fieldId: Short): _root_.scala.Option[TFieldBlob] = {
lazy val _buff = new TMemoryBuffer(32)
lazy val _oprot = new TCompactProtocol(_buff)
_passthroughFields.get(_fieldId) match {
case blob: _root_.scala.Some[TFieldBlob] => blob
case _root_.scala.None => {
val _fieldOpt: _root_.scala.Option[TField] =
_fieldId match {
case 1 =>
if (typeLabel.isDefined) {
writeTypeLabelValue(typeLabel.get, _oprot)
_root_.scala.Some(QAndAAtom.TypeLabelField)
} else {
_root_.scala.None
}
case 3 =>
if (eventImage.isDefined) {
writeEventImageValue(eventImage.get, _oprot)
_root_.scala.Some(QAndAAtom.EventImageField)
} else {
_root_.scala.None
}
case 4 =>
if (item ne null) {
writeItemValue(item, _oprot)
_root_.scala.Some(QAndAAtom.ItemField)
} else {
_root_.scala.None
}
case 5 =>
if (question.isDefined) {
writeQuestionValue(question.get, _oprot)
_root_.scala.Some(QAndAAtom.QuestionField)
} else {
_root_.scala.None
}
case _ => _root_.scala.None
}
_fieldOpt match {
case _root_.scala.Some(_field) =>
val _data = Arrays.copyOfRange(_buff.getArray, 0, _buff.length)
_root_.scala.Some(TFieldBlob(_field, _data))
case _root_.scala.None =>
_root_.scala.None
}
}
}
}
/**
* Collects TCompactProtocol-encoded field values according to `getFieldBlob` into a map.
*/
def getFieldBlobs(ids: TraversableOnce[Short]): immutable$Map[Short, TFieldBlob] =
(ids flatMap { id => getFieldBlob(id) map { id -> _ } }).toMap
/**
* Sets a field using a TCompactProtocol-encoded binary blob. If the field is a known
* field, the blob is decoded and the field is set to the decoded value. If the field
* is unknown and passthrough fields are enabled, then the blob will be stored in
* _passthroughFields.
*/
def setField(_blob: TFieldBlob): QAndAAtom = {
var typeLabel: _root_.scala.Option[String] = this.typeLabel
var eventImage: _root_.scala.Option[com.gu.contentatom.thrift.Image] = this.eventImage
var item: com.gu.contentatom.thrift.atom.qanda.QAndAItem = this.item
var question: _root_.scala.Option[com.gu.contentatom.thrift.atom.storyquestions.Question] = this.question
var _passthroughFields = this._passthroughFields
_blob.id match {
case 1 =>
typeLabel = _root_.scala.Some(readTypeLabelValue(_blob.read))
case 3 =>
eventImage = _root_.scala.Some(readEventImageValue(_blob.read))
case 4 =>
item = readItemValue(_blob.read)
case 5 =>
question = _root_.scala.Some(readQuestionValue(_blob.read))
case _ => _passthroughFields += (_blob.id -> _blob)
}
new Immutable(
typeLabel,
eventImage,
item,
question,
_passthroughFields
)
}
/**
* If the specified field is optional, it is set to None. Otherwise, if the field is
* known, it is reverted to its default value; if the field is unknown, it is removed
* from the passthroughFields map, if present.
*/
def unsetField(_fieldId: Short): QAndAAtom = {
var typeLabel: _root_.scala.Option[String] = this.typeLabel
var eventImage: _root_.scala.Option[com.gu.contentatom.thrift.Image] = this.eventImage
var item: com.gu.contentatom.thrift.atom.qanda.QAndAItem = this.item
var question: _root_.scala.Option[com.gu.contentatom.thrift.atom.storyquestions.Question] = this.question
_fieldId match {
case 1 =>
typeLabel = _root_.scala.None
case 3 =>
eventImage = _root_.scala.None
case 4 =>
item = null
case 5 =>
question = _root_.scala.None
case _ =>
}
new Immutable(
typeLabel,
eventImage,
item,
question,
_passthroughFields - _fieldId
)
}
/**
* If the specified field is optional, it is set to None. Otherwise, if the field is
* known, it is reverted to its default value; if the field is unknown, it is removed
* from the passthroughFields map, if present.
*/
def unsetTypeLabel: QAndAAtom = unsetField(1)
def unsetEventImage: QAndAAtom = unsetField(3)
def unsetItem: QAndAAtom = unsetField(4)
def unsetQuestion: QAndAAtom = unsetField(5)
override def write(_oprot: TProtocol): Unit = {
QAndAAtom.validate(this)
_oprot.writeStructBegin(Struct)
if (typeLabel.isDefined) writeTypeLabelField(typeLabel.get, _oprot)
if (eventImage.isDefined) writeEventImageField(eventImage.get, _oprot)
if (item ne null) writeItemField(item, _oprot)
if (question.isDefined) writeQuestionField(question.get, _oprot)
if (_passthroughFields.nonEmpty) {
_passthroughFields.values.foreach { _.write(_oprot) }
}
_oprot.writeFieldStop()
_oprot.writeStructEnd()
}
def copy(
typeLabel: _root_.scala.Option[String] = this.typeLabel,
eventImage: _root_.scala.Option[com.gu.contentatom.thrift.Image] = this.eventImage,
item: com.gu.contentatom.thrift.atom.qanda.QAndAItem = this.item,
question: _root_.scala.Option[com.gu.contentatom.thrift.atom.storyquestions.Question] = this.question,
_passthroughFields: immutable$Map[Short, TFieldBlob] = this._passthroughFields
): QAndAAtom =
new Immutable(
typeLabel,
eventImage,
item,
question,
_passthroughFields
)
override def canEqual(other: Any): Boolean = other.isInstanceOf[QAndAAtom]
private def _equals(x: QAndAAtom, y: QAndAAtom): Boolean =
x.productArity == y.productArity &&
x.productIterator.sameElements(y.productIterator)
override def equals(other: Any): Boolean =
canEqual(other) &&
_equals(this, other.asInstanceOf[QAndAAtom]) &&
_passthroughFields == other.asInstanceOf[QAndAAtom]._passthroughFields
override def hashCode: Int = _root_.scala.runtime.ScalaRunTime._hashCode(this)
override def toString: String = _root_.scala.runtime.ScalaRunTime._toString(this)
override def productArity: Int = 4
override def productElement(n: Int): Any = n match {
case 0 => this.typeLabel
case 1 => this.eventImage
case 2 => this.item
case 3 => this.question
case _ => throw new IndexOutOfBoundsException(n.toString)
}
override def productPrefix: String = "QAndAAtom"
def _codec: ThriftStructCodec3[QAndAAtom] = QAndAAtom
}
override def decode(_iprot: TProtocol): QAndAAtom =
_iprot match {
case i: LazyTProtocol => lazyDecode(i)
case i => eagerDecode(i)
}
private[this] def eagerDecode(_iprot: TProtocol): QAndAAtom = {
var typeLabel: _root_.scala.Option[String] = _root_.scala.None
var eventImage: _root_.scala.Option[com.gu.contentatom.thrift.Image] = _root_.scala.None
var item: com.gu.contentatom.thrift.atom.qanda.QAndAItem = null
var _got_item = false
var question: _root_.scala.Option[com.gu.contentatom.thrift.atom.storyquestions.Question] = _root_.scala.None
var _passthroughFields: Builder[(Short, TFieldBlob), immutable$Map[Short, TFieldBlob]] = null
var _done = false
_iprot.readStructBegin()
while (!_done) {
val _field = _iprot.readFieldBegin()
if (_field.`type` == TType.STOP) {
_done = true
} else {
_field.id match {
case 1 =>
_field.`type` match {
case TType.STRING =>
typeLabel = _root_.scala.Some(readTypeLabelValue(_iprot))
case _actualType =>
val _expectedType = TType.STRING
throw new TProtocolException(
"Received wrong type for field 'typeLabel' (expected=%s, actual=%s).".format(
ttypeToString(_expectedType),
ttypeToString(_actualType)
)
)
}
case 3 =>
_field.`type` match {
case TType.STRUCT =>
eventImage = _root_.scala.Some(readEventImageValue(_iprot))
case _actualType =>
val _expectedType = TType.STRUCT
throw new TProtocolException(
"Received wrong type for field 'eventImage' (expected=%s, actual=%s).".format(
ttypeToString(_expectedType),
ttypeToString(_actualType)
)
)
}
case 4 =>
_field.`type` match {
case TType.STRUCT =>
item = readItemValue(_iprot)
_got_item = true
case _actualType =>
val _expectedType = TType.STRUCT
throw new TProtocolException(
"Received wrong type for field 'item' (expected=%s, actual=%s).".format(
ttypeToString(_expectedType),
ttypeToString(_actualType)
)
)
}
case 5 =>
_field.`type` match {
case TType.STRUCT =>
question = _root_.scala.Some(readQuestionValue(_iprot))
case _actualType =>
val _expectedType = TType.STRUCT
throw new TProtocolException(
"Received wrong type for field 'question' (expected=%s, actual=%s).".format(
ttypeToString(_expectedType),
ttypeToString(_actualType)
)
)
}
case _ =>
if (_passthroughFields == null)
_passthroughFields = immutable$Map.newBuilder[Short, TFieldBlob]
_passthroughFields += (_field.id -> TFieldBlob.read(_field, _iprot))
}
_iprot.readFieldEnd()
}
}
_iprot.readStructEnd()
if (!_got_item) throw new TProtocolException("Required field 'item' was not found in serialized data for struct QAndAAtom")
new Immutable(
typeLabel,
eventImage,
item,
question,
if (_passthroughFields == null)
NoPassthroughFields
else
_passthroughFields.result()
)
}
def apply(
typeLabel: _root_.scala.Option[String] = _root_.scala.None,
eventImage: _root_.scala.Option[com.gu.contentatom.thrift.Image] = _root_.scala.None,
item: com.gu.contentatom.thrift.atom.qanda.QAndAItem,
question: _root_.scala.Option[com.gu.contentatom.thrift.atom.storyquestions.Question] = _root_.scala.None
): QAndAAtom =
new Immutable(
typeLabel,
eventImage,
item,
question
)
def unapply(_item: QAndAAtom): _root_.scala.Option[_root_.scala.Tuple4[Option[String], Option[com.gu.contentatom.thrift.Image], com.gu.contentatom.thrift.atom.qanda.QAndAItem, Option[com.gu.contentatom.thrift.atom.storyquestions.Question]]] = _root_.scala.Some(_item.toTuple)
@inline private def readTypeLabelValue(_iprot: TProtocol): String = {
_iprot.readString()
}
@inline private def writeTypeLabelField(typeLabel_item: String, _oprot: TProtocol): Unit = {
_oprot.writeFieldBegin(TypeLabelField)
writeTypeLabelValue(typeLabel_item, _oprot)
_oprot.writeFieldEnd()
}
@inline private def writeTypeLabelValue(typeLabel_item: String, _oprot: TProtocol): Unit = {
_oprot.writeString(typeLabel_item)
}
@inline private def readEventImageValue(_iprot: TProtocol): com.gu.contentatom.thrift.Image = {
com.gu.contentatom.thrift.Image.decode(_iprot)
}
@inline private def writeEventImageField(eventImage_item: com.gu.contentatom.thrift.Image, _oprot: TProtocol): Unit = {
_oprot.writeFieldBegin(EventImageField)
writeEventImageValue(eventImage_item, _oprot)
_oprot.writeFieldEnd()
}
@inline private def writeEventImageValue(eventImage_item: com.gu.contentatom.thrift.Image, _oprot: TProtocol): Unit = {
eventImage_item.write(_oprot)
}
@inline private def readItemValue(_iprot: TProtocol): com.gu.contentatom.thrift.atom.qanda.QAndAItem = {
com.gu.contentatom.thrift.atom.qanda.QAndAItem.decode(_iprot)
}
@inline private def writeItemField(item_item: com.gu.contentatom.thrift.atom.qanda.QAndAItem, _oprot: TProtocol): Unit = {
_oprot.writeFieldBegin(ItemField)
writeItemValue(item_item, _oprot)
_oprot.writeFieldEnd()
}
@inline private def writeItemValue(item_item: com.gu.contentatom.thrift.atom.qanda.QAndAItem, _oprot: TProtocol): Unit = {
item_item.write(_oprot)
}
@inline private def readQuestionValue(_iprot: TProtocol): com.gu.contentatom.thrift.atom.storyquestions.Question = {
com.gu.contentatom.thrift.atom.storyquestions.Question.decode(_iprot)
}
@inline private def writeQuestionField(question_item: com.gu.contentatom.thrift.atom.storyquestions.Question, _oprot: TProtocol): Unit = {
_oprot.writeFieldBegin(QuestionField)
writeQuestionValue(question_item, _oprot)
_oprot.writeFieldEnd()
}
@inline private def writeQuestionValue(question_item: com.gu.contentatom.thrift.atom.storyquestions.Question, _oprot: TProtocol): Unit = {
question_item.write(_oprot)
}
object Immutable extends ThriftStructCodec3[QAndAAtom] {
override def encode(_item: QAndAAtom, _oproto: TProtocol): Unit = { _item.write(_oproto) }
override def decode(_iprot: TProtocol): QAndAAtom = QAndAAtom.decode(_iprot)
override lazy val metaData: ThriftStructMetaData[QAndAAtom] = QAndAAtom.metaData
}
/**
* The default read-only implementation of QAndAAtom. You typically should not need to
* directly reference this class; instead, use the QAndAAtom.apply method to construct
* new instances.
*/
class Immutable(
val typeLabel: _root_.scala.Option[String],
val eventImage: _root_.scala.Option[com.gu.contentatom.thrift.Image],
val item: com.gu.contentatom.thrift.atom.qanda.QAndAItem,
val question: _root_.scala.Option[com.gu.contentatom.thrift.atom.storyquestions.Question],
override val _passthroughFields: immutable$Map[Short, TFieldBlob])
extends QAndAAtom {
def this(
typeLabel: _root_.scala.Option[String] = _root_.scala.None,
eventImage: _root_.scala.Option[com.gu.contentatom.thrift.Image] = _root_.scala.None,
item: com.gu.contentatom.thrift.atom.qanda.QAndAItem,
question: _root_.scala.Option[com.gu.contentatom.thrift.atom.storyquestions.Question] = _root_.scala.None
) = this(
typeLabel,
eventImage,
item,
question,
Map.empty
)
}
/**
* This is another Immutable, this however keeps strings as lazy values that are lazily decoded from the backing
* array byte on read.
*/
private[this] class LazyImmutable(
_proto: LazyTProtocol,
_buf: Array[Byte],
_start_offset: Int,
_end_offset: Int,
typeLabelOffset: Int,
val eventImage: _root_.scala.Option[com.gu.contentatom.thrift.Image],
val item: com.gu.contentatom.thrift.atom.qanda.QAndAItem,
val question: _root_.scala.Option[com.gu.contentatom.thrift.atom.storyquestions.Question],
override val _passthroughFields: immutable$Map[Short, TFieldBlob])
extends QAndAAtom {
override def write(_oprot: TProtocol): Unit = {
_oprot match {
case i: LazyTProtocol => i.writeRaw(_buf, _start_offset, _end_offset - _start_offset)
case _ => super.write(_oprot)
}
}
lazy val typeLabel: _root_.scala.Option[String] =
if (typeLabelOffset == -1)
None
else {
Some(_proto.decodeString(_buf, typeLabelOffset))
}
/**
* Override the super hash code to make it a lazy val rather than def.
*
* Calculating the hash code can be expensive, caching it where possible
* can provide significant performance wins. (Key in a hash map for instance)
* Usually not safe since the normal constructor will accept a mutable map or
* set as an arg
* Here however we control how the class is generated from serialized data.
* With the class private and the contract that we throw away our mutable references
* having the hash code lazy here is safe.
*/
override lazy val hashCode = super.hashCode
}
/**
* This Proxy trait allows you to extend the QAndAAtom trait with additional state or
* behavior and implement the read-only methods from QAndAAtom using an underlying
* instance.
*/
trait Proxy extends QAndAAtom {
protected def _underlying_QAndAAtom: QAndAAtom
override def typeLabel: _root_.scala.Option[String] = _underlying_QAndAAtom.typeLabel
override def eventImage: _root_.scala.Option[com.gu.contentatom.thrift.Image] = _underlying_QAndAAtom.eventImage
override def item: com.gu.contentatom.thrift.atom.qanda.QAndAItem = _underlying_QAndAAtom.item
override def question: _root_.scala.Option[com.gu.contentatom.thrift.atom.storyquestions.Question] = _underlying_QAndAAtom.question
override def _passthroughFields = _underlying_QAndAAtom._passthroughFields
}
}
Shapeless
- Library for generic programming in Scala
- Leverages the similarities between types
case class Employee(name: String, age: Int, manager: Boolean)
case class IceCream(name: String, numCherries: Int, inCone: Boolean)
String :: Int :: Boolean :: HNil
HList
Generic[IceCream].to(iceCream)
Generic[Employee].to(employee)
First approach
case class Atom(
id: String,
atomType: String,
title: String,
data: AtomData)
sealed trait AtomData
object AtomData {
case class Media(media: MediaAtom) extends AtomData
case class Explainer(explainer: ExplainerAtom) extends AtomData
}
case class MediaAtom(
url: String,
metadata: Option[Metadata])
case class ExplainerAtom(
headline: String,
body: String)
case class Metadata(
commentsEnabled: Option[Boolean],
channelId: Option[String])
First approach
val genericRepr = LabelledGeneric[Atom].to(mediaAtom)
// "123" :: "media" :: "Scala fun" :: Media(MediaAtom(...)) :: HNil
PATCH /api/media/123/title
body: "Updated title"
val updatedGeneric = genericRepr + (Symbol("title") ->> "Updated title"))
// "123" :: "media" :: "Updated title" :: Media(MediaAtom(...)) :: HNil
val mediaAtom = Atom(
id = "123",
atomType = "media",
title = "Scala fun",
data = AtomData.Media(MediaAtom(
url = "gu.com",
metadata = Some(Metadata(
commentsEnabled = Some(true),
channelId = Some("programming"))))))
First approach
val genericRepr = LabelledGeneric[Atom].to(mediaAtom)
// "123" :: "media" :: "Scala fun" :: Media(MediaAtom(...)) :: HNil
PATCH /api/media/123/data.metadata.channelId
body: "Scala"
val updatedGeneric2 = genericRepr + (Symbol(nestedField) ->> "Updated field"))
// error: Expression scala.Symbol.apply(ScalaFiddle.this.nestedField) does
// not evaluate to a constant or a stable reference value
val nestedField = "data.metadata.channelId"
val mediaAtom = Atom(
id = "123",
atomType = "media",
title = "Scala fun",
data = AtomData.Media(MediaAtom(
url = "gu.com",
metadata = Some(Metadata(
commentsEnabled = Some(true),
channelId = Some("programming"))))))
Second approach
Nested Map[String, Any]
Update the field in the map
HList
HList
Type Classes
trait Greetings[A] {
def message(creature: A): String
}
object Greetings {
implicit def humanMessage: Greetings[Human] =
(human: Human) => s"${human.name} says hello."
implicit def catMessage: Greetings[Cat] =
(cat: Cat) => s"${cat.name} says meow."
implicit class GreetingsOps[A](creature: A) {
def greet(implicit greetings: Greetings[A]) = {
greetings.message(creature)
}
}
}
Human("Maria").greet
// "Maria says hello."
Cat("Mistoffelees").greet
// "Mistoffelees says meow."
Dog("Pollicle").greet
// error: could not find implicit value for parameter message
Case class => Map[String,Any]
trait ToMapRec[L <: HList] {
def apply(l: L): Map[String, Any]
}
"1234567" :: Some("media") :: "Scala fun" :: Media(MediaAtom(...)) :: HNil
...
// Implicits:
HNil
FieldType[K, Option[H]] :: T
FieldType[K, Seq[H]] :: T
FieldType[K, H] :: T // Where the head can be recursively converted
FieldType[K, H] :: T // Where the head can NOT be recursively converted (low priority)
Case class => Map[String,Any]
trait ToMapRec[L <: HList] {
def apply(hlist: L): Map[String, Any]
}
...
implicit def hconsToMapRecOption[K <: Symbol, V, H <: HList, T <: HList]
(implicit
wit: Witness.Aux[K],
gen: LabelledGeneric.Aux[V, H],
tmrT: Lazy[ToMapRec[T]],
tmrH: Lazy[ToMapRec[H]]
): ToMapRec[FieldType[K, Option[V]] :: T] = ???
...
Case class => Map[String,Any]
trait ToMapRec[L <: HList] {
def apply(hlist: L): Map[String, Any]
}
...
implicit def hconsToMapRecOption[K <: Symbol, V, H <: HList, T <: HList]
(implicit
wit: Witness.Aux[K],
gen: LabelledGeneric.Aux[V, H],
tmrT: Lazy[ToMapRec[T]],
tmrH: Lazy[ToMapRec[H]]
): ToMapRec[FieldType[K, Option[V]] :: T] =
(hlist: FieldType[K, Option[V]] :: T) =>
tmrT.value(hlist.tail) +
(wit.value.name -> hlist.head.map(value => tmrH.value(gen.to(value))))
Case class => Map[String,Any]
...
implicit class ToMapRecOps[A](val a: A) extends AnyVal {
def toMap[L <: HList]
(implicit
gen: LabelledGeneric.Aux[A, L],
tmr: Lazy[ToMapRec[L]]
): Map[String, Any] = tmr.value(gen.to(a))
}
val atomDataMap = atomData.toMap
val atomDataMap = Map(
"metadata" -> Some(Map(
"commentsEnabled" -> Some(false),
"channelId" -> Some("programming"))),
"url" -> "gu.com")
[error] could not find implicit value for parameter
gen: shapeless.LabelledGeneric.Aux[com.gu.contentatom.thrift.AtomData,L]
Fixing Thrift
def helperLabelledGeneric[A, I <: A, L <: HList]
(implicit gen: LabelledGeneric.Aux[I, L])
: LabelledGeneric.Aux[A, L] =
new LabelledGeneric[A] {
override type Repr = L
override def to(t: A): Repr = gen.to(t.asInstanceOf[I])
override def from(r: Repr): A = gen.from(r)
}
implicit def atomDataLabelledGeneric[L <: HList]
(implicit gen: LabelledGeneric.Aux[AtomData.Immutable, L])
: LabelledGeneric.Aux[AtomData, L] =
helperLabelledGeneric[AtomData, AtomData.Immutable, L]
...
Map[String,Any] => Case class
// Implicits:
HNil
FieldType[K, Option[H]] :: T
FieldType[K, Seq[H]] :: T
FieldType[K, H] :: T // Where the head can be recursively converted
FieldType[K, H] :: T // Where the head can NOT be recursively converted (low priority)
trait FromMap[L <: HList] {
def apply(m: Map[String, Any]): Option[L]
}
Map[String,Any] => Case class
trait FromMap[L <: HList] {
def apply(m: Map[String, Any]): Option[L]
}
...
implicit def hconsFromMapOption[K <: Symbol, V, R <: HList, T <: HList]
(implicit
witness: Witness.Aux[K],
gen: LabelledGeneric.Aux[V, R],
fromMapH: Lazy[FromMap[R]],
fromMapT: Lazy[FromMap[T]]
): FromMap[FieldType[K, Option[V]] :: T] = (m: Map[String, Any]) =>
m(witness.value.name) match {
case Some(v) =>
for {
r <- Typeable[Option[Map[String, Any]]].cast(Some(v))
h <- r.map(fromMapH.value(_))
t <- fromMapT.value(m)
} yield field[K](h.map(gen.from)) :: t
case None =>
for {
t <- fromMapT.value(m)
} yield field[K](None) :: t
}
...
Map[String,Any] => Case class
...
implicit class FromMapOps[A] {
def from[R <: HList](m: Map[String, Any])
(implicit gen: LabelledGeneric.Aux[A, R],
fromMap: Lazy[FromMap[R]]
): Option[A] = fromMap.value(m).map(gen.from)
}
val atom: Atom = to[Atom.Immutable].from(updatedAtom)
Final approach
Json + Circe
Circe
PATCH /api/media/some-id
body:
{
"data": {
"metadata": {
"channelId": "Scala"
}
}
}
val atomJson = atom.toJson
atomJson.deepMerge(bodyJson)
Bonus: Magnolia
import magnolia._
import scala.language.experimental.macros
trait ToMapMagnolia[T] {
def apply(arg: T) : Any
}
object ToMapMagnolia {
type Typeclass[T] = ToMapMagnolia[T]
def combine[T](caseClass: CaseClass[Typeclass, T]): Typeclass[T] =
(instance: T) => caseClass.parameters.map(param =>
param.label -> {param.typeclass(param.dereference(instance))}).toMap
def dispatch[T](sealedTrait: SealedTrait[Typeclass, T]): Typeclass[T] =
(instance: T) => sealedTrait.dispatch(instance)(subType =>
subType.typeclass.apply(subType.cast(instance)))
implicit def baseCase[T <: AnyVal]: ToMapMagnolia[T] = (x: T) => x
implicit def stringCase: ToMapMagnolia[String] = x => x
implicit def optCase[T](implicit tm: Typeclass[T]): Typeclass[Option[T]] = _.map(tm.apply)
implicit def seqCase[T](implicit tm: Typeclass[T]): Typeclass[Seq[T]] = _.map(tm.apply)
implicit def gen[T]: Typeclass[T] = macro Magnolia.gen[T]
implicit class ToMapOps[A](a: A) {
def toMapMagnolia(implicit tm: ToMapMagnolia[A]): Any = tm(a)
}
}
Summary
- New and shiny is fun.
- Shapeless book: The Type Astronaut's Guide to Shapeless
- Code: github.com/marialivia16/scrooge-shapeless-maps
- Magnolia: http://magnolia.work
- Twitter: @MariaLiviaCh
Thank you.
The Path to Generic Endpoints using Shapeless
By Maria Livia
The Path to Generic Endpoints using Shapeless
Scala eXchange
- 2,106